Commit | Line | Data |
---|---|---|
2be4d090 MD |
1 | <?php |
2 | ||
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/>. | |
17 | ||
18 | ||
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'; | |
28 | ||
29 | ||
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() { | |
40 | ||
41 | completion_cron_mark_started(); | |
42 | ||
43 | completion_cron_criteria(); | |
44 | ||
45 | completion_cron_completions(); | |
46 | } | |
47 | ||
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; | |
55 | ||
56 | if (debugging()) { | |
57 | mtrace('Marking users as started'); | |
58 | } | |
59 | ||
1c6ed505 | 60 | if (!empty($CFG->gradebookroles)) { |
61 | $roles = ' AND ra.roleid IN ('.$CFG->gradebookroles.')'; | |
24a3b341 AB |
62 | } else { |
63 | // This causes it to default to everyone (if there is no student role) | |
64 | $roles = ''; | |
65 | } | |
66 | ||
89482538 AB |
67 | /** |
68 | * A quick explaination of this horrible looking query | |
69 | * | |
70 | * It's purpose is to locate all the active participants | |
71 | * of a course with course completion enabled. | |
72 | * | |
73 | * We also only want the users with no course_completions | |
74 | * record as this functions job is to create the missing | |
75 | * ones :) | |
76 | * | |
77 | * We want to record the user's enrolment start time for the | |
78 | * course. This gets tricky because there can be multiple | |
79 | * enrolment plugins active in a course, hence the possibility | |
80 | * of multiple records for each couse/user in the results | |
81 | */ | |
2be4d090 | 82 | $sql = " |
89482538 | 83 | SELECT |
2be4d090 | 84 | c.id AS course, |
89482538 | 85 | u.id AS userid, |
2be4d090 | 86 | crc.id AS completionid, |
24a3b341 AB |
87 | ue.timestart AS timeenrolled, |
88 | ue.timecreated | |
2be4d090 | 89 | FROM |
89482538 AB |
90 | {user} u |
91 | INNER JOIN | |
92 | {user_enrolments} ue | |
93 | ON ue.userid = u.id | |
2be4d090 | 94 | INNER JOIN |
89482538 AB |
95 | {enrol} e |
96 | ON e.id = ue.enrolid | |
2be4d090 | 97 | INNER JOIN |
89482538 AB |
98 | {course} c |
99 | ON c.id = e.courseid | |
24a3b341 AB |
100 | INNER JOIN |
101 | {role_assignments} ra | |
102 | ON ra.userid = u.id | |
2be4d090 MD |
103 | LEFT JOIN |
104 | {course_completions} crc | |
105 | ON crc.course = c.id | |
89482538 | 106 | AND crc.userid = u.id |
2be4d090 | 107 | WHERE |
89482538 | 108 | c.enablecompletion = 1 |
2be4d090 | 109 | AND crc.timeenrolled IS NULL |
89482538 AB |
110 | AND ue.status = 0 |
111 | AND e.status = 0 | |
112 | AND u.deleted = 0 | |
113 | AND ue.timestart < ? | |
114 | AND (ue.timeend > ? OR ue.timeend = 0) | |
24a3b341 | 115 | $roles |
2be4d090 MD |
116 | ORDER BY |
117 | course, | |
118 | userid | |
119 | "; | |
120 | ||
121 | // Check if result is empty | |
89482538 AB |
122 | $now = time(); |
123 | if (!$rs = $DB->get_recordset_sql($sql, array($now, $now, $now, $now))) { | |
2be4d090 MD |
124 | return; |
125 | } | |
126 | ||
89482538 AB |
127 | /** |
128 | * An explaination of the following loop | |
129 | * | |
130 | * We are essentially doing a group by in the code here (as I can't find | |
131 | * a decent way of doing it in the sql). | |
132 | * | |
133 | * Since there can be multiple enrolment plugins for each course, we can have | |
134 | * multiple rows for each particpant in the query result. This isn't really | |
135 | * a problem until you combine it with the fact that the enrolment plugins | |
136 | * can save the enrol start time in either timestart or timeenrolled. | |
137 | * | |
138 | * The purpose of this loop is to find the earliest enrolment start time for | |
139 | * each participant in each course. | |
140 | */ | |
141 | $prev = null; | |
142 | while ($rs->valid() || $prev) { | |
143 | ||
144 | $current = $rs->current(); | |
145 | ||
146 | if (!isset($current->course)) { | |
147 | $current = false; | |
148 | } | |
149 | else { | |
150 | // Not all enrol plugins fill out timestart correctly, so use whichever | |
151 | // is non-zero | |
24a3b341 | 152 | $current->timeenrolled = max($current->timecreated, $current->timeenrolled); |
2be4d090 MD |
153 | } |
154 | ||
89482538 AB |
155 | // If we are at the last record, |
156 | // or we aren't at the first and the record is for a diff user/course | |
157 | if ($prev && | |
158 | (!$rs->valid() || | |
159 | ($current->course != $prev->course || $current->userid != $prev->userid))) { | |
160 | ||
161 | $completion = new completion_completion(); | |
162 | $completion->userid = $prev->userid; | |
163 | $completion->course = $prev->course; | |
164 | $completion->timeenrolled = (string) $prev->timeenrolled; | |
165 | $completion->timestarted = 0; | |
24a3b341 | 166 | $completion->reaggregate = time(); |
89482538 AB |
167 | |
168 | if ($prev->completionid) { | |
169 | $completion->id = $prev->completionid; | |
170 | } | |
2be4d090 | 171 | |
89482538 AB |
172 | $completion->mark_enrolled(); |
173 | ||
174 | if (debugging()) { | |
175 | mtrace('Marked started user '.$prev->userid.' in course '.$prev->course); | |
176 | } | |
2be4d090 | 177 | } |
89482538 AB |
178 | // Else, if this record is for the same user/course |
179 | elseif ($prev && $current) { | |
180 | // Use oldest timeenrolled | |
181 | $current->timeenrolled = min($current->timeenrolled, $prev->timeenrolled); | |
182 | } | |
183 | ||
184 | // Move current record to previous | |
185 | $prev = $current; | |
186 | ||
187 | // Move to next record | |
188 | $rs->next(); | |
2be4d090 MD |
189 | } |
190 | ||
191 | $rs->close(); | |
192 | } | |
193 | ||
194 | /** | |
195 | * Run installed criteria's data aggregation methods | |
196 | * | |
197 | * Loop through each installed criteria and run the | |
198 | * cron() method if it exists | |
199 | * | |
200 | * @return void | |
201 | */ | |
202 | function completion_cron_criteria() { | |
203 | ||
204 | // Process each criteria type | |
205 | global $CFG, $COMPLETION_CRITERIA_TYPES; | |
206 | ||
207 | foreach ($COMPLETION_CRITERIA_TYPES as $type) { | |
208 | ||
209 | $object = 'completion_criteria_'.$type; | |
210 | require_once $CFG->libdir.'/completion/'.$object.'.php'; | |
211 | ||
212 | $class = new $object(); | |
213 | ||
214 | // Run the criteria type's cron method, if it has one | |
215 | if (method_exists($class, 'cron')) { | |
216 | ||
217 | if (debugging()) { | |
218 | mtrace('Running '.$object.'->cron()'); | |
219 | } | |
220 | $class->cron(); | |
221 | } | |
222 | } | |
223 | } | |
224 | ||
225 | /** | |
226 | * Aggregate each user's criteria completions | |
227 | * | |
228 | * @return void | |
229 | */ | |
230 | function completion_cron_completions() { | |
231 | global $DB; | |
232 | ||
233 | if (debugging()) { | |
234 | mtrace('Aggregating completions'); | |
235 | } | |
236 | ||
237 | // Save time started | |
238 | $timestarted = time(); | |
239 | ||
240 | // Grab all criteria and their associated criteria completions | |
241 | $sql = ' | |
242 | SELECT DISTINCT | |
243 | c.id AS course, | |
244 | cr.id AS criteriaid, | |
24a3b341 | 245 | crc.userid AS userid, |
2be4d090 MD |
246 | cr.criteriatype AS criteriatype, |
247 | cc.timecompleted AS timecompleted | |
248 | FROM | |
249 | {course_completion_criteria} cr | |
250 | INNER JOIN | |
251 | {course} c | |
252 | ON cr.course = c.id | |
253 | INNER JOIN | |
89482538 AB |
254 | {course_completions} crc |
255 | ON crc.course = c.id | |
2be4d090 MD |
256 | LEFT JOIN |
257 | {course_completion_crit_compl} cc | |
258 | ON cc.criteriaid = cr.id | |
89482538 | 259 | AND crc.userid = cc.userid |
2be4d090 | 260 | WHERE |
89482538 | 261 | c.enablecompletion = 1 |
2be4d090 MD |
262 | AND crc.timecompleted IS NULL |
263 | AND crc.reaggregate > 0 | |
24a3b341 | 264 | AND crc.reaggregate < :timestarted |
2be4d090 MD |
265 | ORDER BY |
266 | course, | |
267 | userid | |
268 | '; | |
269 | ||
270 | // Check if result is empty | |
24a3b341 | 271 | if (!$rs = $DB->get_recordset_sql($sql, array('timestarted' => $timestarted))) { |
2be4d090 MD |
272 | return; |
273 | } | |
274 | ||
275 | $current_user = null; | |
276 | $current_course = null; | |
277 | $completions = array(); | |
278 | ||
279 | while (1) { | |
280 | ||
281 | // Grab records for current user/course | |
282 | foreach ($rs as $record) { | |
283 | // If we are still grabbing the same users completions | |
284 | if ($record->userid === $current_user && $record->course === $current_course) { | |
285 | $completions[$record->criteriaid] = $record; | |
286 | } else { | |
287 | break; | |
288 | } | |
289 | } | |
290 | ||
291 | // Aggregate | |
292 | if (!empty($completions)) { | |
293 | ||
294 | if (debugging()) { | |
295 | mtrace('Aggregating completions for user '.$current_user.' in course '.$current_course); | |
296 | } | |
297 | ||
298 | // Get course info object | |
299 | $info = new completion_info((object)array('id' => $current_course)); | |
300 | ||
301 | // Setup aggregation | |
302 | $overall = $info->get_aggregation_method(); | |
303 | $activity = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ACTIVITY); | |
304 | $prerequisite = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_COURSE); | |
305 | $role = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ROLE); | |
306 | ||
307 | $overall_status = null; | |
308 | $activity_status = null; | |
309 | $prerequisite_status = null; | |
310 | $role_status = null; | |
311 | ||
312 | // Get latest timecompleted | |
313 | $timecompleted = null; | |
314 | ||
315 | // Check each of the criteria | |
316 | foreach ($completions as $params) { | |
317 | $timecompleted = max($timecompleted, $params->timecompleted); | |
318 | ||
319 | $completion = new completion_criteria_completion($params, false); | |
320 | ||
321 | // Handle aggregation special cases | |
322 | if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) { | |
323 | completion_cron_aggregate($activity, $completion->is_complete(), $activity_status); | |
324 | } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) { | |
325 | completion_cron_aggregate($prerequisite, $completion->is_complete(), $prerequisite_status); | |
326 | } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ROLE) { | |
327 | completion_cron_aggregate($role, $completion->is_complete(), $role_status); | |
328 | } else { | |
329 | completion_cron_aggregate($overall, $completion->is_complete(), $overall_status); | |
330 | } | |
331 | } | |
332 | ||
333 | // Include role criteria aggregation in overall aggregation | |
334 | if ($role_status !== null) { | |
335 | completion_cron_aggregate($overall, $role_status, $overall_status); | |
336 | } | |
337 | ||
338 | // Include activity criteria aggregation in overall aggregation | |
339 | if ($activity_status !== null) { | |
340 | completion_cron_aggregate($overall, $activity_status, $overall_status); | |
341 | } | |
342 | ||
343 | // Include prerequisite criteria aggregation in overall aggregation | |
344 | if ($prerequisite_status !== null) { | |
345 | completion_cron_aggregate($overall, $prerequisite_status, $overall_status); | |
346 | } | |
347 | ||
348 | // If aggregation status is true, mark course complete for user | |
349 | if ($overall_status) { | |
350 | if (debugging()) { | |
351 | mtrace('Marking complete'); | |
352 | } | |
353 | ||
354 | $ccompletion = new completion_completion(array('course' => $params->course, 'userid' => $params->userid)); | |
355 | $ccompletion->mark_complete($timecompleted); | |
356 | } | |
357 | } | |
358 | ||
359 | // If this is the end of the recordset, break the loop | |
360 | if (!$rs->valid()) { | |
361 | $rs->close(); | |
362 | break; | |
363 | } | |
364 | ||
365 | // New/next user, update user details, reset completions | |
366 | $current_user = $record->userid; | |
367 | $current_course = $record->course; | |
368 | $completions = array(); | |
369 | $completions[$record->criteriaid] = $record; | |
370 | } | |
371 | ||
372 | // Mark all users as aggregated | |
373 | $sql = " | |
374 | UPDATE | |
375 | {course_completions} | |
376 | SET | |
377 | reaggregate = 0 | |
378 | WHERE | |
379 | reaggregate < {$timestarted} | |
380 | "; | |
381 | ||
382 | $DB->execute($sql); | |
383 | } | |
384 | ||
385 | /** | |
386 | * Aggregate criteria status's as per configured aggregation method | |
387 | * | |
388 | * @param int $method COMPLETION_AGGREGATION_* constant | |
389 | * @param bool $data Criteria completion status | |
390 | * @param bool|null $state Aggregation state | |
391 | * @return void | |
392 | */ | |
393 | function completion_cron_aggregate($method, $data, &$state) { | |
394 | if ($method == COMPLETION_AGGREGATION_ALL) { | |
395 | if ($data && $state !== false) { | |
396 | $state = true; | |
397 | } else { | |
398 | $state = false; | |
399 | } | |
400 | } elseif ($method == COMPLETION_AGGREGATION_ANY) { | |
401 | if ($data) { | |
402 | $state = true; | |
403 | } else if (!$data && $state === null) { | |
404 | $state = false; | |
405 | } | |
406 | } | |
407 | } |