PageRenderTime 58ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/completion/cron.php

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