MDL-21782 reworked enrolment framework, the core infrastructure is in place, the...
[moodle.git] / lib / completion / cron.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
19 /**
20  * Cron job for reviewing and aggregating course completion criteria
21  *
22  * @package   moodlecore
23  * @copyright 2009 Catalyst IT Ltd
24  * @author    Aaron Barnes <aaronb@catalyst.net.nz>
25  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
27 require_once $CFG->libdir.'/completionlib.php';
30 /**
31  * Update user's course completion statuses
32  *
33  * First update all criteria completions, then
34  * aggregate all criteria completions and update
35  * overall course completions
36  *
37  * @return  void
38  */
39 function completion_cron() {
41     completion_cron_mark_started();
43     completion_cron_criteria();
45     completion_cron_completions();
46 }
48 /**
49  * Mark users as started if the config option is set
50  *
51  * @return  void
52  */
53 function completion_cron_mark_started() {
54     global $CFG, $DB;
56 //TODO: MDL-22797 completion needs to be updated to use new enrolment framework
58     if (debugging()) {
59         mtrace('Marking users as started');
60     }
62     $roles = '';
63     if (!empty($CFG->progresstrackedroles)) {
64         $roles = 'AND ra.roleid IN ('.$CFG->progresstrackedroles.')';
65     }
67     $sql = "
68         SELECT DISTINCT
69             c.id AS course,
70             ra.userid AS userid,
71             crc.id AS completionid,
72             MIN(ra.timestart) AS timestarted
73         FROM
74             {course} c
75         INNER JOIN
76             {context} con
77          ON con.instanceid = c.id
78         INNER JOIN
79             {role_assignments} ra
80          ON ra.contextid = con.id
81         LEFT JOIN
82             {course_completions} crc
83          ON crc.course = c.id
84         AND crc.userid = ra.userid
85         WHERE
86             con.contextlevel = ".CONTEXT_COURSE."
87         AND c.enablecompletion = 1
88         AND c.completionstartonenrol = 1
89         AND crc.timeenrolled IS NULL
90         AND (ra.timeend IS NULL OR ra.timeend > ".time().")
91         {$roles}
92         GROUP BY
93             c.id,
94             ra.userid,
95             crc.id
96         ORDER BY
97             course,
98             userid
99     ";
101     // Check if result is empty
102     if (!$rs = $DB->get_recordset_sql($sql)) {
103         return;
104     }
106     // Grab records for current user/course
107     foreach ($rs as $record) {
108         $completion = new completion_completion();
109         $completion->userid = $record->userid;
110         $completion->course = $record->course;
111         $completion->timeenrolled = $record->timestarted;
113         if ($record->completionid) {
114             $completion->id = $record->completionid;
115         }
117         $completion->mark_enrolled();
119         if (debugging()) {
120             mtrace('Marked started user '.$record->userid.' in course '.$record->course);
121         }
122     }
124     $rs->close();
127 /**
128  * Run installed criteria's data aggregation methods
129  *
130  * Loop through each installed criteria and run the
131  * cron() method if it exists
132  *
133  * @return  void
134  */
135 function completion_cron_criteria() {
137     // Process each criteria type
138     global $CFG, $COMPLETION_CRITERIA_TYPES;
140     foreach ($COMPLETION_CRITERIA_TYPES as $type) {
142         $object = 'completion_criteria_'.$type;
143         require_once $CFG->libdir.'/completion/'.$object.'.php';
145         $class = new $object();
147         // Run the criteria type's cron method, if it has one
148         if (method_exists($class, 'cron')) {
150             if (debugging()) {
151                 mtrace('Running '.$object.'->cron()');
152             }
153             $class->cron();
154         }
155     }
158 /**
159  * Aggregate each user's criteria completions
160  *
161  * @return  void
162  */
163 function completion_cron_completions() {
164     global $DB;
166     if (debugging()) {
167         mtrace('Aggregating completions');
168     }
170     // Save time started
171     $timestarted = time();
173     // Grab all criteria and their associated criteria completions
174     $sql = '
175         SELECT DISTINCT
176             c.id AS course,
177             cr.id AS criteriaid,
178             ra.userid AS userid,
179             cr.criteriatype AS criteriatype,
180             cc.timecompleted AS timecompleted
181         FROM
182             {course_completion_criteria} cr
183         INNER JOIN
184             {course} c
185          ON cr.course = c.id
186         INNER JOIN
187             {context} con
188          ON con.instanceid = c.id
189         INNER JOIN
190             {role_assignments} ra
191          ON ra.contextid = con.id
192         LEFT JOIN
193             {course_completion_crit_compl} cc
194          ON cc.criteriaid = cr.id
195         AND cc.userid = ra.userid
196         LEFT JOIN
197             {course_completions} crc
198          ON crc.course = c.id
199         AND crc.userid = ra.userid
200         WHERE
201             con.contextlevel = '.CONTEXT_COURSE.'
202         AND c.enablecompletion = 1
203         AND crc.timecompleted IS NULL
204         AND crc.reaggregate > 0
205         ORDER BY
206             course,
207             userid
208     ';
210     // Check if result is empty
211     if (!$rs = $DB->get_recordset_sql($sql)) {
212         return;
213     }
215     $current_user = null;
216     $current_course = null;
217     $completions = array();
219     while (1) {
221         // Grab records for current user/course
222         foreach ($rs as $record) {
223             // If we are still grabbing the same users completions
224             if ($record->userid === $current_user && $record->course === $current_course) {
225                 $completions[$record->criteriaid] = $record;
226             } else {
227                 break;
228             }
229         }
231         // Aggregate
232         if (!empty($completions)) {
234             if (debugging()) {
235                 mtrace('Aggregating completions for user '.$current_user.' in course '.$current_course);
236             }
238             // Get course info object
239             $info = new completion_info((object)array('id' => $current_course));
241             // Setup aggregation
242             $overall = $info->get_aggregation_method();
243             $activity = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ACTIVITY);
244             $prerequisite = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_COURSE);
245             $role = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ROLE);
247             $overall_status = null;
248             $activity_status = null;
249             $prerequisite_status = null;
250             $role_status = null;
252             // Get latest timecompleted
253             $timecompleted = null;
255             // Check each of the criteria
256             foreach ($completions as $params) {
257                 $timecompleted = max($timecompleted, $params->timecompleted);
259                 $completion = new completion_criteria_completion($params, false);
261                 // Handle aggregation special cases
262                 if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
263                     completion_cron_aggregate($activity, $completion->is_complete(), $activity_status);
264                 } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) {
265                     completion_cron_aggregate($prerequisite, $completion->is_complete(), $prerequisite_status);
266                 } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ROLE) {
267                     completion_cron_aggregate($role, $completion->is_complete(), $role_status);
268                 } else {
269                     completion_cron_aggregate($overall, $completion->is_complete(), $overall_status);
270                 }
271             }
273             // Include role criteria aggregation in overall aggregation
274             if ($role_status !== null) {
275                 completion_cron_aggregate($overall, $role_status, $overall_status);
276             }
278             // Include activity criteria aggregation in overall aggregation
279             if ($activity_status !== null) {
280                 completion_cron_aggregate($overall, $activity_status, $overall_status);
281             }
283             // Include prerequisite criteria aggregation in overall aggregation
284             if ($prerequisite_status !== null) {
285                 completion_cron_aggregate($overall, $prerequisite_status, $overall_status);
286             }
288             // If aggregation status is true, mark course complete for user
289             if ($overall_status) {
290                 if (debugging()) {
291                     mtrace('Marking complete');
292                 }
294                 $ccompletion = new completion_completion(array('course' => $params->course, 'userid' => $params->userid));
295                 $ccompletion->mark_complete($timecompleted);
296             }
297         }
299         // If this is the end of the recordset, break the loop
300         if (!$rs->valid()) {
301             $rs->close();
302             break;
303         }
305         // New/next user, update user details, reset completions
306         $current_user = $record->userid;
307         $current_course = $record->course;
308         $completions = array();
309         $completions[$record->criteriaid] = $record;
310     }
312     // Mark all users as aggregated
313     $sql = "
314         UPDATE
315             {course_completions}
316         SET
317             reaggregate = 0
318         WHERE
319             reaggregate < {$timestarted}
320     ";
322     $DB->execute($sql);
325 /**
326  * Aggregate criteria status's as per configured aggregation method
327  *
328  * @param int $method COMPLETION_AGGREGATION_* constant
329  * @param bool $data Criteria completion status
330  * @param bool|null $state Aggregation state
331  * @return void
332  */
333 function completion_cron_aggregate($method, $data, &$state) {
334     if ($method == COMPLETION_AGGREGATION_ALL) {
335         if ($data && $state !== false) {
336             $state = true;
337         } else {
338             $state = false;
339         }
340     } elseif ($method == COMPLETION_AGGREGATION_ANY) {
341         if ($data) {
342             $state = true;
343         } else if (!$data && $state === null) {
344             $state = false;
345         }
346     }