MDL-34332 completion: timeenrolled not always set correctly
[moodle.git] / completion / cron.php
CommitLineData
2be4d090 1<?php
2be4d090
MD
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
2be4d090
MD
17/**
18 * Cron job for reviewing and aggregating course completion criteria
19 *
836375ec
SH
20 * @package core_completion
21 * @category completion
2be4d090 22 * @copyright 2009 Catalyst IT Ltd
836375ec
SH
23 * @author Aaron Barnes <aaronb@catalyst.net.nz>
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2be4d090 25 */
2be4d090 26
836375ec 27defined('MOODLE_INTERNAL') || die();
f55ff38a 28require_once($CFG->libdir.'/completionlib.php');
2be4d090
MD
29
30/**
31 * Update user's course completion statuses
32 *
836375ec
SH
33 * First update all criteria completions, then aggregate all criteria completions
34 * and update overall course completions
2be4d090
MD
35 */
36function completion_cron() {
37
38 completion_cron_mark_started();
39
40 completion_cron_criteria();
41
42 completion_cron_completions();
43}
44
45/**
46 * Mark users as started if the config option is set
47 *
836375ec 48 * @return void
2be4d090
MD
49 */
50function completion_cron_mark_started() {
154a72b0 51 global $DB;
2be4d090
MD
52
53 if (debugging()) {
54 mtrace('Marking users as started');
55 }
56
89482538
AB
57 /**
58 * A quick explaination of this horrible looking query
59 *
60 * It's purpose is to locate all the active participants
61 * of a course with course completion enabled.
62 *
63 * We also only want the users with no course_completions
64 * record as this functions job is to create the missing
65 * ones :)
66 *
67 * We want to record the user's enrolment start time for the
68 * course. This gets tricky because there can be multiple
69 * enrolment plugins active in a course, hence the possibility
70 * of multiple records for each couse/user in the results
71 */
2be4d090 72 $sql = "
154a72b0
AB
73 INSERT INTO
74 {course_completions}
75 (course, userid, timeenrolled, timestarted, reaggregate)
89482538 76 SELECT
2be4d090 77 c.id AS course,
154a72b0
AB
78 ue.userid AS userid,
79 CASE
80 WHEN MIN(ue.timestart) <> 0
81 THEN MIN(ue.timestart)
82 ELSE ?
83 END,
84 0,
85 ?
2be4d090 86 FROM
89482538 87 {user_enrolments} ue
2be4d090 88 INNER JOIN
89482538
AB
89 {enrol} e
90 ON e.id = ue.enrolid
2be4d090 91 INNER JOIN
89482538
AB
92 {course} c
93 ON c.id = e.courseid
2be4d090
MD
94 LEFT JOIN
95 {course_completions} crc
96 ON crc.course = c.id
154a72b0 97 AND crc.userid = ue.userid
2be4d090 98 WHERE
89482538 99 c.enablecompletion = 1
154a72b0
AB
100 AND crc.id IS NULL
101 AND ue.status = ?
102 AND e.status = ?
89482538
AB
103 AND ue.timestart < ?
104 AND (ue.timeend > ? OR ue.timeend = 0)
154a72b0
AB
105 GROUP BY
106 c.id,
107 ue.userid
2be4d090
MD
108 ";
109
89482538 110 $now = time();
154a72b0
AB
111 $params = array(
112 $now,
113 $now,
114 ENROL_USER_ACTIVE,
115 ENROL_INSTANCE_ENABLED,
116 $now,
117 $now
118 );
119 $affected = $DB->execute($sql, $params, true);
2be4d090
MD
120}
121
122/**
123 * Run installed criteria's data aggregation methods
124 *
125 * Loop through each installed criteria and run the
126 * cron() method if it exists
127 *
836375ec 128 * @return void
2be4d090
MD
129 */
130function completion_cron_criteria() {
131
132 // Process each criteria type
133 global $CFG, $COMPLETION_CRITERIA_TYPES;
134
135 foreach ($COMPLETION_CRITERIA_TYPES as $type) {
136
137 $object = 'completion_criteria_'.$type;
60829d80 138 require_once $CFG->dirroot.'/completion/criteria/'.$object.'.php';
2be4d090
MD
139
140 $class = new $object();
141
142 // Run the criteria type's cron method, if it has one
143 if (method_exists($class, 'cron')) {
144
145 if (debugging()) {
146 mtrace('Running '.$object.'->cron()');
147 }
148 $class->cron();
149 }
150 }
151}
152
153/**
154 * Aggregate each user's criteria completions
2be4d090
MD
155 */
156function completion_cron_completions() {
157 global $DB;
158
159 if (debugging()) {
160 mtrace('Aggregating completions');
161 }
162
163 // Save time started
164 $timestarted = time();
165
166 // Grab all criteria and their associated criteria completions
167 $sql = '
168 SELECT DISTINCT
169 c.id AS course,
170 cr.id AS criteriaid,
24a3b341 171 crc.userid AS userid,
2be4d090
MD
172 cr.criteriatype AS criteriatype,
173 cc.timecompleted AS timecompleted
174 FROM
175 {course_completion_criteria} cr
176 INNER JOIN
177 {course} c
178 ON cr.course = c.id
179 INNER JOIN
89482538
AB
180 {course_completions} crc
181 ON crc.course = c.id
2be4d090
MD
182 LEFT JOIN
183 {course_completion_crit_compl} cc
184 ON cc.criteriaid = cr.id
89482538 185 AND crc.userid = cc.userid
2be4d090 186 WHERE
89482538 187 c.enablecompletion = 1
2be4d090
MD
188 AND crc.timecompleted IS NULL
189 AND crc.reaggregate > 0
24a3b341 190 AND crc.reaggregate < :timestarted
2be4d090
MD
191 ORDER BY
192 course,
193 userid
194 ';
195
419178d7
EL
196 $rs = $DB->get_recordset_sql($sql, array('timestarted' => $timestarted));
197
2be4d090 198 // Check if result is empty
419178d7
EL
199 if (!$rs->valid()) {
200 $rs->close(); // Not going to iterate (but exit), close rs
2be4d090
MD
201 return;
202 }
203
204 $current_user = null;
205 $current_course = null;
206 $completions = array();
207
208 while (1) {
209
210 // Grab records for current user/course
211 foreach ($rs as $record) {
212 // If we are still grabbing the same users completions
213 if ($record->userid === $current_user && $record->course === $current_course) {
214 $completions[$record->criteriaid] = $record;
215 } else {
216 break;
217 }
218 }
219
220 // Aggregate
221 if (!empty($completions)) {
222
223 if (debugging()) {
224 mtrace('Aggregating completions for user '.$current_user.' in course '.$current_course);
225 }
226
227 // Get course info object
228 $info = new completion_info((object)array('id' => $current_course));
229
230 // Setup aggregation
231 $overall = $info->get_aggregation_method();
232 $activity = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ACTIVITY);
233 $prerequisite = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_COURSE);
234 $role = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ROLE);
235
236 $overall_status = null;
237 $activity_status = null;
238 $prerequisite_status = null;
239 $role_status = null;
240
241 // Get latest timecompleted
242 $timecompleted = null;
243
244 // Check each of the criteria
245 foreach ($completions as $params) {
246 $timecompleted = max($timecompleted, $params->timecompleted);
247
dbfcf440 248 $completion = new completion_criteria_completion((array)$params, false);
2be4d090
MD
249
250 // Handle aggregation special cases
251 if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
252 completion_cron_aggregate($activity, $completion->is_complete(), $activity_status);
253 } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) {
254 completion_cron_aggregate($prerequisite, $completion->is_complete(), $prerequisite_status);
255 } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ROLE) {
256 completion_cron_aggregate($role, $completion->is_complete(), $role_status);
257 } else {
258 completion_cron_aggregate($overall, $completion->is_complete(), $overall_status);
259 }
260 }
261
262 // Include role criteria aggregation in overall aggregation
263 if ($role_status !== null) {
264 completion_cron_aggregate($overall, $role_status, $overall_status);
265 }
266
267 // Include activity criteria aggregation in overall aggregation
268 if ($activity_status !== null) {
269 completion_cron_aggregate($overall, $activity_status, $overall_status);
270 }
271
272 // Include prerequisite criteria aggregation in overall aggregation
273 if ($prerequisite_status !== null) {
274 completion_cron_aggregate($overall, $prerequisite_status, $overall_status);
275 }
276
277 // If aggregation status is true, mark course complete for user
278 if ($overall_status) {
279 if (debugging()) {
280 mtrace('Marking complete');
281 }
282
283 $ccompletion = new completion_completion(array('course' => $params->course, 'userid' => $params->userid));
284 $ccompletion->mark_complete($timecompleted);
285 }
286 }
287
288 // If this is the end of the recordset, break the loop
289 if (!$rs->valid()) {
290 $rs->close();
291 break;
292 }
293
294 // New/next user, update user details, reset completions
295 $current_user = $record->userid;
296 $current_course = $record->course;
297 $completions = array();
298 $completions[$record->criteriaid] = $record;
299 }
300
301 // Mark all users as aggregated
302 $sql = "
303 UPDATE
304 {course_completions}
305 SET
306 reaggregate = 0
307 WHERE
7ba1c665
AB
308 reaggregate < :timestarted
309 AND reaggregate > 0
2be4d090
MD
310 ";
311
7ba1c665 312 $DB->execute($sql, array('timestarted' => $timestarted));
2be4d090
MD
313}
314
315/**
316 * Aggregate criteria status's as per configured aggregation method
317 *
318 * @param int $method COMPLETION_AGGREGATION_* constant
319 * @param bool $data Criteria completion status
320 * @param bool|null $state Aggregation state
2be4d090
MD
321 */
322function completion_cron_aggregate($method, $data, &$state) {
323 if ($method == COMPLETION_AGGREGATION_ALL) {
324 if ($data && $state !== false) {
325 $state = true;
326 } else {
327 $state = false;
328 }
329 } elseif ($method == COMPLETION_AGGREGATION_ANY) {
330 if ($data) {
331 $state = true;
332 } else if (!$data && $state === null) {
333 $state = false;
334 }
335 }
336}