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