PageRenderTime 44ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/completion/cron.php

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