course completion MDL-2631 Course completion feature - Thanks to Aaron Barnes and...
[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
60 $roles = '';
61 if (!empty($CFG->progresstrackedroles)) {
62 $roles = 'AND ra.roleid IN ('.$CFG->progresstrackedroles.')';
63 }
64
65 $sql = "
66 SELECT DISTINCT
67 c.id AS course,
68 ra.userid AS userid,
69 crc.id AS completionid,
70 MIN(ra.timestart) AS timestarted
71 FROM
72 {course} c
73 INNER JOIN
74 {context} con
75 ON con.instanceid = c.id
76 INNER JOIN
77 {role_assignments} ra
78 ON ra.contextid = con.id
79 LEFT JOIN
80 {course_completions} crc
81 ON crc.course = c.id
82 AND crc.userid = ra.userid
83 WHERE
84 con.contextlevel = ".CONTEXT_COURSE."
85 AND c.enablecompletion = 1
86 AND c.completionstartonenrol = 1
87 AND crc.timeenrolled IS NULL
88 AND (ra.timeend IS NULL OR ra.timeend > ".time().")
89 {$roles}
90 GROUP BY
91 c.id,
92 ra.userid,
93 crc.id
94 ORDER BY
95 course,
96 userid
97 ";
98
99 // Check if result is empty
100 if (!$rs = $DB->get_recordset_sql($sql)) {
101 return;
102 }
103
104 // Grab records for current user/course
105 foreach ($rs as $record) {
106 $completion = new completion_completion();
107 $completion->userid = $record->userid;
108 $completion->course = $record->course;
109 $completion->timeenrolled = $record->timestarted;
110
111 if ($record->completionid) {
112 $completion->id = $record->completionid;
113 }
114
115 $completion->mark_enrolled();
116
117 if (debugging()) {
118 mtrace('Marked started user '.$record->userid.' in course '.$record->course);
119 }
120 }
121
122 $rs->close();
123}
124
125/**
126 * Run installed criteria's data aggregation methods
127 *
128 * Loop through each installed criteria and run the
129 * cron() method if it exists
130 *
131 * @return void
132 */
133function completion_cron_criteria() {
134
135 // Process each criteria type
136 global $CFG, $COMPLETION_CRITERIA_TYPES;
137
138 foreach ($COMPLETION_CRITERIA_TYPES as $type) {
139
140 $object = 'completion_criteria_'.$type;
141 require_once $CFG->libdir.'/completion/'.$object.'.php';
142
143 $class = new $object();
144
145 // Run the criteria type's cron method, if it has one
146 if (method_exists($class, 'cron')) {
147
148 if (debugging()) {
149 mtrace('Running '.$object.'->cron()');
150 }
151 $class->cron();
152 }
153 }
154}
155
156/**
157 * Aggregate each user's criteria completions
158 *
159 * @return void
160 */
161function completion_cron_completions() {
162 global $DB;
163
164 if (debugging()) {
165 mtrace('Aggregating completions');
166 }
167
168 // Save time started
169 $timestarted = time();
170
171 // Grab all criteria and their associated criteria completions
172 $sql = '
173 SELECT DISTINCT
174 c.id AS course,
175 cr.id AS criteriaid,
176 ra.userid AS userid,
177 cr.criteriatype AS criteriatype,
178 cc.timecompleted AS timecompleted
179 FROM
180 {course_completion_criteria} cr
181 INNER JOIN
182 {course} c
183 ON cr.course = c.id
184 INNER JOIN
185 {context} con
186 ON con.instanceid = c.id
187 INNER JOIN
188 {role_assignments} ra
189 ON ra.contextid = con.id
190 LEFT JOIN
191 {course_completion_crit_compl} cc
192 ON cc.criteriaid = cr.id
193 AND cc.userid = ra.userid
194 LEFT JOIN
195 {course_completions} crc
196 ON crc.course = c.id
197 AND crc.userid = ra.userid
198 WHERE
199 con.contextlevel = '.CONTEXT_COURSE.'
200 AND c.enablecompletion = 1
201 AND crc.timecompleted IS NULL
202 AND crc.reaggregate > 0
203 ORDER BY
204 course,
205 userid
206 ';
207
208 // Check if result is empty
209 if (!$rs = $DB->get_recordset_sql($sql)) {
210 return;
211 }
212
213 $current_user = null;
214 $current_course = null;
215 $completions = array();
216
217 while (1) {
218
219 // Grab records for current user/course
220 foreach ($rs as $record) {
221 // If we are still grabbing the same users completions
222 if ($record->userid === $current_user && $record->course === $current_course) {
223 $completions[$record->criteriaid] = $record;
224 } else {
225 break;
226 }
227 }
228
229 // Aggregate
230 if (!empty($completions)) {
231
232 if (debugging()) {
233 mtrace('Aggregating completions for user '.$current_user.' in course '.$current_course);
234 }
235
236 // Get course info object
237 $info = new completion_info((object)array('id' => $current_course));
238
239 // Setup aggregation
240 $overall = $info->get_aggregation_method();
241 $activity = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ACTIVITY);
242 $prerequisite = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_COURSE);
243 $role = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ROLE);
244
245 $overall_status = null;
246 $activity_status = null;
247 $prerequisite_status = null;
248 $role_status = null;
249
250 // Get latest timecompleted
251 $timecompleted = null;
252
253 // Check each of the criteria
254 foreach ($completions as $params) {
255 $timecompleted = max($timecompleted, $params->timecompleted);
256
257 $completion = new completion_criteria_completion($params, false);
258
259 // Handle aggregation special cases
260 if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
261 completion_cron_aggregate($activity, $completion->is_complete(), $activity_status);
262 } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) {
263 completion_cron_aggregate($prerequisite, $completion->is_complete(), $prerequisite_status);
264 } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ROLE) {
265 completion_cron_aggregate($role, $completion->is_complete(), $role_status);
266 } else {
267 completion_cron_aggregate($overall, $completion->is_complete(), $overall_status);
268 }
269 }
270
271 // Include role criteria aggregation in overall aggregation
272 if ($role_status !== null) {
273 completion_cron_aggregate($overall, $role_status, $overall_status);
274 }
275
276 // Include activity criteria aggregation in overall aggregation
277 if ($activity_status !== null) {
278 completion_cron_aggregate($overall, $activity_status, $overall_status);
279 }
280
281 // Include prerequisite criteria aggregation in overall aggregation
282 if ($prerequisite_status !== null) {
283 completion_cron_aggregate($overall, $prerequisite_status, $overall_status);
284 }
285
286 // If aggregation status is true, mark course complete for user
287 if ($overall_status) {
288 if (debugging()) {
289 mtrace('Marking complete');
290 }
291
292 $ccompletion = new completion_completion(array('course' => $params->course, 'userid' => $params->userid));
293 $ccompletion->mark_complete($timecompleted);
294 }
295 }
296
297 // If this is the end of the recordset, break the loop
298 if (!$rs->valid()) {
299 $rs->close();
300 break;
301 }
302
303 // New/next user, update user details, reset completions
304 $current_user = $record->userid;
305 $current_course = $record->course;
306 $completions = array();
307 $completions[$record->criteriaid] = $record;
308 }
309
310 // Mark all users as aggregated
311 $sql = "
312 UPDATE
313 {course_completions}
314 SET
315 reaggregate = 0
316 WHERE
317 reaggregate < {$timestarted}
318 ";
319
320 $DB->execute($sql);
321}
322
323/**
324 * Aggregate criteria status's as per configured aggregation method
325 *
326 * @param int $method COMPLETION_AGGREGATION_* constant
327 * @param bool $data Criteria completion status
328 * @param bool|null $state Aggregation state
329 * @return void
330 */
331function completion_cron_aggregate($method, $data, &$state) {
332 if ($method == COMPLETION_AGGREGATION_ALL) {
333 if ($data && $state !== false) {
334 $state = true;
335 } else {
336 $state = false;
337 }
338 } elseif ($method == COMPLETION_AGGREGATION_ANY) {
339 if ($data) {
340 $state = true;
341 } else if (!$data && $state === null) {
342 $state = false;
343 }
344 }
345}