PageRenderTime 42ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/completion/cron.php

http://github.com/moodle/moodle
PHP | 413 lines | 237 code | 60 blank | 116 comment | 50 complexity | 8c3fed23a33a59c74bdcc63434ed4a4a MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  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/>.
  16. /**
  17. * Cron job for reviewing and aggregating course completion criteria
  18. *
  19. * @package moodlecore
  20. * @copyright 2009 Catalyst IT Ltd
  21. * @author Aaron Barnes <aaronb@catalyst.net.nz>
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. require_once $CFG->libdir.'/completionlib.php';
  25. /**
  26. * Update user's course completion statuses
  27. *
  28. * First update all criteria completions, then
  29. * aggregate all criteria completions and update
  30. * overall course completions
  31. *
  32. * @return void
  33. */
  34. function completion_cron() {
  35. completion_cron_mark_started();
  36. completion_cron_criteria();
  37. completion_cron_completions();
  38. }
  39. /**
  40. * Mark users as started if the config option is set
  41. *
  42. * @return void
  43. */
  44. function completion_cron_mark_started() {
  45. global $CFG, $DB;
  46. if (debugging()) {
  47. mtrace('Marking users as started');
  48. }
  49. if (!empty($CFG->gradebookroles)) {
  50. $roles = ' AND ra.roleid IN ('.$CFG->gradebookroles.')';
  51. } else {
  52. // This causes it to default to everyone (if there is no student role)
  53. $roles = '';
  54. }
  55. /**
  56. * A quick explaination of this horrible looking query
  57. *
  58. * It's purpose is to locate all the active participants
  59. * of a course with course completion enabled.
  60. *
  61. * We also only want the users with no course_completions
  62. * record as this functions job is to create the missing
  63. * ones :)
  64. *
  65. * We want to record the user's enrolment start time for the
  66. * course. This gets tricky because there can be multiple
  67. * enrolment plugins active in a course, hence the possibility
  68. * of multiple records for each couse/user in the results
  69. */
  70. $sql = "
  71. SELECT
  72. c.id AS course,
  73. u.id AS userid,
  74. crc.id AS completionid,
  75. ue.timestart AS timeenrolled,
  76. ue.timecreated
  77. FROM
  78. {user} u
  79. INNER JOIN
  80. {user_enrolments} ue
  81. ON ue.userid = u.id
  82. INNER JOIN
  83. {enrol} e
  84. ON e.id = ue.enrolid
  85. INNER JOIN
  86. {course} c
  87. ON c.id = e.courseid
  88. INNER JOIN
  89. {role_assignments} ra
  90. ON ra.userid = u.id
  91. LEFT JOIN
  92. {course_completions} crc
  93. ON crc.course = c.id
  94. AND crc.userid = u.id
  95. WHERE
  96. c.enablecompletion = 1
  97. AND crc.timeenrolled IS NULL
  98. AND ue.status = 0
  99. AND e.status = 0
  100. AND u.deleted = 0
  101. AND ue.timestart < ?
  102. AND (ue.timeend > ? OR ue.timeend = 0)
  103. $roles
  104. ORDER BY
  105. course,
  106. userid
  107. ";
  108. $now = time();
  109. $rs = $DB->get_recordset_sql($sql, array($now, $now, $now, $now));
  110. // Check if result is empty
  111. if (!$rs->valid()) {
  112. $rs->close(); // Not going to iterate (but exit), close rs
  113. return;
  114. }
  115. /**
  116. * An explaination of the following loop
  117. *
  118. * We are essentially doing a group by in the code here (as I can't find
  119. * a decent way of doing it in the sql).
  120. *
  121. * Since there can be multiple enrolment plugins for each course, we can have
  122. * multiple rows for each particpant in the query result. This isn't really
  123. * a problem until you combine it with the fact that the enrolment plugins
  124. * can save the enrol start time in either timestart or timeenrolled.
  125. *
  126. * The purpose of this loop is to find the earliest enrolment start time for
  127. * each participant in each course.
  128. */
  129. $prev = null;
  130. while ($rs->valid() || $prev) {
  131. $current = $rs->current();
  132. if (!isset($current->course)) {
  133. $current = false;
  134. }
  135. else {
  136. // Not all enrol plugins fill out timestart correctly, so use whichever
  137. // is non-zero
  138. $current->timeenrolled = max($current->timecreated, $current->timeenrolled);
  139. }
  140. // If we are at the last record,
  141. // or we aren't at the first and the record is for a diff user/course
  142. if ($prev &&
  143. (!$rs->valid() ||
  144. ($current->course != $prev->course || $current->userid != $prev->userid))) {
  145. $completion = new completion_completion();
  146. $completion->userid = $prev->userid;
  147. $completion->course = $prev->course;
  148. $completion->timeenrolled = (string) $prev->timeenrolled;
  149. $completion->timestarted = 0;
  150. $completion->reaggregate = time();
  151. if ($prev->completionid) {
  152. $completion->id = $prev->completionid;
  153. }
  154. $completion->mark_enrolled();
  155. if (debugging()) {
  156. mtrace('Marked started user '.$prev->userid.' in course '.$prev->course);
  157. }
  158. }
  159. // Else, if this record is for the same user/course
  160. elseif ($prev && $current) {
  161. // Use oldest timeenrolled
  162. $current->timeenrolled = min($current->timeenrolled, $prev->timeenrolled);
  163. }
  164. // Move current record to previous
  165. $prev = $current;
  166. // Move to next record
  167. $rs->next();
  168. }
  169. $rs->close();
  170. }
  171. /**
  172. * Run installed criteria's data aggregation methods
  173. *
  174. * Loop through each installed criteria and run the
  175. * cron() method if it exists
  176. *
  177. * @return void
  178. */
  179. function completion_cron_criteria() {
  180. // Process each criteria type
  181. global $CFG, $COMPLETION_CRITERIA_TYPES;
  182. foreach ($COMPLETION_CRITERIA_TYPES as $type) {
  183. $object = 'completion_criteria_'.$type;
  184. require_once $CFG->libdir.'/completion/'.$object.'.php';
  185. $class = new $object();
  186. // Run the criteria type's cron method, if it has one
  187. if (method_exists($class, 'cron')) {
  188. if (debugging()) {
  189. mtrace('Running '.$object.'->cron()');
  190. }
  191. $class->cron();
  192. }
  193. }
  194. }
  195. /**
  196. * Aggregate each user's criteria completions
  197. *
  198. * @return void
  199. */
  200. function completion_cron_completions() {
  201. global $DB;
  202. if (debugging()) {
  203. mtrace('Aggregating completions');
  204. }
  205. // Save time started
  206. $timestarted = time();
  207. // Grab all criteria and their associated criteria completions
  208. $sql = '
  209. SELECT DISTINCT
  210. c.id AS course,
  211. cr.id AS criteriaid,
  212. crc.userid AS userid,
  213. cr.criteriatype AS criteriatype,
  214. cc.timecompleted AS timecompleted
  215. FROM
  216. {course_completion_criteria} cr
  217. INNER JOIN
  218. {course} c
  219. ON cr.course = c.id
  220. INNER JOIN
  221. {course_completions} crc
  222. ON crc.course = c.id
  223. LEFT JOIN
  224. {course_completion_crit_compl} cc
  225. ON cc.criteriaid = cr.id
  226. AND crc.userid = cc.userid
  227. WHERE
  228. c.enablecompletion = 1
  229. AND crc.timecompleted IS NULL
  230. AND crc.reaggregate > 0
  231. AND crc.reaggregate < :timestarted
  232. ORDER BY
  233. course,
  234. userid
  235. ';
  236. $rs = $DB->get_recordset_sql($sql, array('timestarted' => $timestarted));
  237. // Check if result is empty
  238. if (!$rs->valid()) {
  239. $rs->close(); // Not going to iterate (but exit), close rs
  240. return;
  241. }
  242. $current_user = null;
  243. $current_course = null;
  244. $completions = array();
  245. while (1) {
  246. // Grab records for current user/course
  247. foreach ($rs as $record) {
  248. // If we are still grabbing the same users completions
  249. if ($record->userid === $current_user && $record->course === $current_course) {
  250. $completions[$record->criteriaid] = $record;
  251. } else {
  252. break;
  253. }
  254. }
  255. // Aggregate
  256. if (!empty($completions)) {
  257. if (debugging()) {
  258. mtrace('Aggregating completions for user '.$current_user.' in course '.$current_course);
  259. }
  260. // Get course info object
  261. $info = new completion_info((object)array('id' => $current_course));
  262. // Setup aggregation
  263. $overall = $info->get_aggregation_method();
  264. $activity = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ACTIVITY);
  265. $prerequisite = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_COURSE);
  266. $role = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ROLE);
  267. $overall_status = null;
  268. $activity_status = null;
  269. $prerequisite_status = null;
  270. $role_status = null;
  271. // Get latest timecompleted
  272. $timecompleted = null;
  273. // Check each of the criteria
  274. foreach ($completions as $params) {
  275. $timecompleted = max($timecompleted, $params->timecompleted);
  276. $completion = new completion_criteria_completion($params, false);
  277. // Handle aggregation special cases
  278. if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
  279. completion_cron_aggregate($activity, $completion->is_complete(), $activity_status);
  280. } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) {
  281. completion_cron_aggregate($prerequisite, $completion->is_complete(), $prerequisite_status);
  282. } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ROLE) {
  283. completion_cron_aggregate($role, $completion->is_complete(), $role_status);
  284. } else {
  285. completion_cron_aggregate($overall, $completion->is_complete(), $overall_status);
  286. }
  287. }
  288. // Include role criteria aggregation in overall aggregation
  289. if ($role_status !== null) {
  290. completion_cron_aggregate($overall, $role_status, $overall_status);
  291. }
  292. // Include activity criteria aggregation in overall aggregation
  293. if ($activity_status !== null) {
  294. completion_cron_aggregate($overall, $activity_status, $overall_status);
  295. }
  296. // Include prerequisite criteria aggregation in overall aggregation
  297. if ($prerequisite_status !== null) {
  298. completion_cron_aggregate($overall, $prerequisite_status, $overall_status);
  299. }
  300. // If aggregation status is true, mark course complete for user
  301. if ($overall_status) {
  302. if (debugging()) {
  303. mtrace('Marking complete');
  304. }
  305. $ccompletion = new completion_completion(array('course' => $params->course, 'userid' => $params->userid));
  306. $ccompletion->mark_complete($timecompleted);
  307. }
  308. }
  309. // If this is the end of the recordset, break the loop
  310. if (!$rs->valid()) {
  311. $rs->close();
  312. break;
  313. }
  314. // New/next user, update user details, reset completions
  315. $current_user = $record->userid;
  316. $current_course = $record->course;
  317. $completions = array();
  318. $completions[$record->criteriaid] = $record;
  319. }
  320. // Mark all users as aggregated
  321. $sql = "
  322. UPDATE
  323. {course_completions}
  324. SET
  325. reaggregate = 0
  326. WHERE
  327. reaggregate < {$timestarted}
  328. ";
  329. $DB->execute($sql);
  330. }
  331. /**
  332. * Aggregate criteria status's as per configured aggregation method
  333. *
  334. * @param int $method COMPLETION_AGGREGATION_* constant
  335. * @param bool $data Criteria completion status
  336. * @param bool|null $state Aggregation state
  337. * @return void
  338. */
  339. function completion_cron_aggregate($method, $data, &$state) {
  340. if ($method == COMPLETION_AGGREGATION_ALL) {
  341. if ($data && $state !== false) {
  342. $state = true;
  343. } else {
  344. $state = false;
  345. }
  346. } elseif ($method == COMPLETION_AGGREGATION_ANY) {
  347. if ($data) {
  348. $state = true;
  349. } else if (!$data && $state === null) {
  350. $state = false;
  351. }
  352. }
  353. }