MDL-25708 recordsets - fix events/grade/group/moodle libs
[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
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 */
202function 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 */
230function 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 */
393function 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}