MDL-34332 completion: timeenrolled not always set correctly
[moodle.git] / completion / cron.php
1 <?php
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/>.
17 /**
18  * Cron job for reviewing and aggregating course completion criteria
19  *
20  * @package core_completion
21  * @category completion
22  * @copyright 2009 Catalyst IT Ltd
23  * @author Aaron Barnes <aaronb@catalyst.net.nz>
24  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->libdir.'/completionlib.php');
30 /**
31  * Update user's course completion statuses
32  *
33  * First update all criteria completions, then aggregate all criteria completions
34  * and update overall course completions
35  */
36 function completion_cron() {
38     completion_cron_mark_started();
40     completion_cron_criteria();
42     completion_cron_completions();
43 }
45 /**
46  * Mark users as started if the config option is set
47  *
48  * @return void
49  */
50 function completion_cron_mark_started() {
51     global $DB;
53     if (debugging()) {
54         mtrace('Marking users as started');
55     }
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      */
72     $sql = "
73         INSERT INTO
74             {course_completions}
75             (course, userid, timeenrolled, timestarted, reaggregate)
76         SELECT
77             c.id AS course,
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             ?
86         FROM
87             {user_enrolments} ue
88         INNER JOIN
89             {enrol} e
90          ON e.id = ue.enrolid
91         INNER JOIN
92             {course} c
93          ON c.id = e.courseid
94         LEFT JOIN
95             {course_completions} crc
96          ON crc.course = c.id
97         AND crc.userid = ue.userid
98         WHERE
99             c.enablecompletion = 1
100         AND crc.id IS NULL
101         AND ue.status = ?
102         AND e.status = ?
103         AND ue.timestart < ?
104         AND (ue.timeend > ? OR ue.timeend = 0)
105         GROUP BY
106             c.id,
107             ue.userid
108     ";
110     $now = time();
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);
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  *
128  * @return void
129  */
130 function completion_cron_criteria() {
132     // Process each criteria type
133     global $CFG, $COMPLETION_CRITERIA_TYPES;
135     foreach ($COMPLETION_CRITERIA_TYPES as $type) {
137         $object = 'completion_criteria_'.$type;
138         require_once $CFG->dirroot.'/completion/criteria/'.$object.'.php';
140         $class = new $object();
142         // Run the criteria type's cron method, if it has one
143         if (method_exists($class, 'cron')) {
145             if (debugging()) {
146                 mtrace('Running '.$object.'->cron()');
147             }
148             $class->cron();
149         }
150     }
153 /**
154  * Aggregate each user's criteria completions
155  */
156 function completion_cron_completions() {
157     global $DB;
159     if (debugging()) {
160         mtrace('Aggregating completions');
161     }
163     // Save time started
164     $timestarted = time();
166     // Grab all criteria and their associated criteria completions
167     $sql = '
168         SELECT DISTINCT
169             c.id AS course,
170             cr.id AS criteriaid,
171             crc.userid AS userid,
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
180             {course_completions} crc
181          ON crc.course = c.id
182         LEFT JOIN
183             {course_completion_crit_compl} cc
184          ON cc.criteriaid = cr.id
185         AND crc.userid = cc.userid
186         WHERE
187             c.enablecompletion = 1
188         AND crc.timecompleted IS NULL
189         AND crc.reaggregate > 0
190         AND crc.reaggregate < :timestarted
191         ORDER BY
192             course,
193             userid
194     ';
196     $rs = $DB->get_recordset_sql($sql, array('timestarted' => $timestarted));
198     // Check if result is empty
199     if (!$rs->valid()) {
200         $rs->close(); // Not going to iterate (but exit), close rs
201         return;
202     }
204     $current_user = null;
205     $current_course = null;
206     $completions = array();
208     while (1) {
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         }
220         // Aggregate
221         if (!empty($completions)) {
223             if (debugging()) {
224                 mtrace('Aggregating completions for user '.$current_user.' in course '.$current_course);
225             }
227             // Get course info object
228             $info = new completion_info((object)array('id' => $current_course));
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);
236             $overall_status = null;
237             $activity_status = null;
238             $prerequisite_status = null;
239             $role_status = null;
241             // Get latest timecompleted
242             $timecompleted = null;
244             // Check each of the criteria
245             foreach ($completions as $params) {
246                 $timecompleted = max($timecompleted, $params->timecompleted);
248                 $completion = new completion_criteria_completion((array)$params, false);
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             }
262             // Include role criteria aggregation in overall aggregation
263             if ($role_status !== null) {
264                 completion_cron_aggregate($overall, $role_status, $overall_status);
265             }
267             // Include activity criteria aggregation in overall aggregation
268             if ($activity_status !== null) {
269                 completion_cron_aggregate($overall, $activity_status, $overall_status);
270             }
272             // Include prerequisite criteria aggregation in overall aggregation
273             if ($prerequisite_status !== null) {
274                 completion_cron_aggregate($overall, $prerequisite_status, $overall_status);
275             }
277             // If aggregation status is true, mark course complete for user
278             if ($overall_status) {
279                 if (debugging()) {
280                     mtrace('Marking complete');
281                 }
283                 $ccompletion = new completion_completion(array('course' => $params->course, 'userid' => $params->userid));
284                 $ccompletion->mark_complete($timecompleted);
285             }
286         }
288         // If this is the end of the recordset, break the loop
289         if (!$rs->valid()) {
290             $rs->close();
291             break;
292         }
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     }
301     // Mark all users as aggregated
302     $sql = "
303         UPDATE
304             {course_completions}
305         SET
306             reaggregate = 0
307         WHERE
308             reaggregate < :timestarted
309         AND reaggregate > 0
310     ";
312     $DB->execute($sql, array('timestarted' => $timestarted));
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
321  */
322 function 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     }