PageRenderTime 50ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/completionlib.php

https://github.com/mylescarrick/moodle
PHP | 1321 lines | 625 code | 170 blank | 526 comment | 108 complexity | 5afd57ddcd9d914c5812a3e4b0b8f73f MD5 | raw file
Possible License(s): GPL-3.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. * Contains a class used for tracking whether activities have been completed
  18. * by students ('completion')
  19. *
  20. * Completion top-level options (admin setting enablecompletion)
  21. *
  22. * @package core
  23. * @subpackage completion
  24. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  25. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  26. */
  27. defined('MOODLE_INTERNAL') || die();
  28. require_once $CFG->libdir.'/completion/completion_aggregation.php';
  29. require_once $CFG->libdir.'/completion/completion_criteria.php';
  30. require_once $CFG->libdir.'/completion/completion_completion.php';
  31. require_once $CFG->libdir.'/completion/completion_criteria_completion.php';
  32. /** The completion system is enabled in this site/course */
  33. define('COMPLETION_ENABLED', 1);
  34. /** The completion system is not enabled in this site/course */
  35. define('COMPLETION_DISABLED', 0);
  36. // Completion tracking options per-activity (course_modules/completion)
  37. /** Completion tracking is disabled for this activity */
  38. define('COMPLETION_TRACKING_NONE', 0);
  39. /** Manual completion tracking (user ticks box) is enabled for this activity */
  40. define('COMPLETION_TRACKING_MANUAL', 1);
  41. /** Automatic completion tracking (system ticks box) is enabled for this activity */
  42. define('COMPLETION_TRACKING_AUTOMATIC', 2);
  43. // Completion state values (course_modules_completion/completionstate)
  44. /** The user has not completed this activity. */
  45. define('COMPLETION_INCOMPLETE', 0);
  46. /** The user has completed this activity. It is not specified whether they have
  47. * passed or failed it. */
  48. define('COMPLETION_COMPLETE', 1);
  49. /** The user has completed this activity with a grade above the pass mark. */
  50. define('COMPLETION_COMPLETE_PASS', 2);
  51. /** The user has completed this activity but their grade is less than the pass mark */
  52. define('COMPLETION_COMPLETE_FAIL', 3);
  53. // Completion effect changes (used only in update_state)
  54. /** The effect of this change to completion status is unknown. */
  55. define('COMPLETION_UNKNOWN', -1);
  56. /** The user's grade has changed, so their new state might be
  57. * COMPLETION_COMPLETE_PASS or COMPLETION_COMPLETE_FAIL. */
  58. // TODO Is this useful?
  59. define('COMPLETION_GRADECHANGE', -2);
  60. // Whether view is required to create an activity (course_modules/completionview)
  61. /** User must view this activity */
  62. define('COMPLETION_VIEW_REQUIRED', 1);
  63. /** User does not need to view this activity */
  64. define('COMPLETION_VIEW_NOT_REQUIRED', 0);
  65. // Completion viewed state (course_modules_completion/viewed)
  66. /** User has viewed this activity */
  67. define('COMPLETION_VIEWED', 1);
  68. /** User has not viewed this activity */
  69. define('COMPLETION_NOT_VIEWED', 0);
  70. // Completion cacheing
  71. /** Cache expiry time in seconds (10 minutes) */
  72. define('COMPLETION_CACHE_EXPIRY', 10*60);
  73. // Combining completion condition. This is also the value you should return
  74. // if you don't have any applicable conditions. Used for activity completion.
  75. /** Completion details should be ORed together and you should return false if
  76. none apply */
  77. define('COMPLETION_OR', false);
  78. /** Completion details should be ANDed together and you should return true if
  79. none apply */
  80. define('COMPLETION_AND', true);
  81. // Course completion criteria aggregation methods
  82. define('COMPLETION_AGGREGATION_ALL', 1);
  83. define('COMPLETION_AGGREGATION_ANY', 2);
  84. /**
  85. * Class represents completion information for a course.
  86. *
  87. * Does not contain any data, so you can safely construct it multiple times
  88. * without causing any problems.
  89. *
  90. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  91. * @package moodlecore
  92. */
  93. class completion_info {
  94. /**
  95. * Course object passed during construction
  96. * @access private
  97. * @var object
  98. */
  99. private $course;
  100. /**
  101. * Course id
  102. * @access public
  103. * @var int
  104. */
  105. public $course_id;
  106. /**
  107. * Completion criteria
  108. * @access private
  109. * @var array
  110. * @see completion_info->get_criteria()
  111. */
  112. private $criteria;
  113. /**
  114. * Return array of aggregation methods
  115. * @access public
  116. * @return array
  117. */
  118. public static function get_aggregation_methods() {
  119. return array(
  120. COMPLETION_AGGREGATION_ALL => get_string('all'),
  121. COMPLETION_AGGREGATION_ANY => get_string('any', 'completion'),
  122. );
  123. }
  124. /**
  125. * Constructs with course details.
  126. *
  127. * @param object $course Moodle course object. Must have at least ->id, ->enablecompletion
  128. */
  129. public function __construct($course) {
  130. $this->course = $course;
  131. $this->course_id = $course->id;
  132. }
  133. /**
  134. * Determines whether completion is enabled across entire site.
  135. *
  136. * Static function.
  137. *
  138. * @global object
  139. * @return int COMPLETION_ENABLED (true) if completion is enabled for the site,
  140. * COMPLETION_DISABLED (false) if it's complete
  141. */
  142. public static function is_enabled_for_site() {
  143. global $CFG;
  144. return !empty($CFG->enablecompletion);
  145. }
  146. /**
  147. * Checks whether completion is enabled in a particular course and possibly
  148. * activity.
  149. *
  150. * @global object
  151. * @uses COMPLETION_DISABLED
  152. * @uses COMPLETION_ENABLED
  153. * @param object $cm Course-module object. If not specified, returns the course
  154. * completion enable state.
  155. * @return mixed COMPLETION_ENABLED or COMPLETION_DISABLED (==0) in the case of
  156. * site and course; COMPLETION_TRACKING_MANUAL, _AUTOMATIC or _NONE (==0)
  157. * for a course-module.
  158. */
  159. public function is_enabled($cm=null) {
  160. global $CFG;
  161. // First check global completion
  162. if (!isset($CFG->enablecompletion) || $CFG->enablecompletion == COMPLETION_DISABLED) {
  163. return COMPLETION_DISABLED;
  164. }
  165. // Check course completion
  166. if ($this->course->enablecompletion == COMPLETION_DISABLED) {
  167. return COMPLETION_DISABLED;
  168. }
  169. // If there was no $cm and we got this far, then it's enabled
  170. if (!$cm) {
  171. return COMPLETION_ENABLED;
  172. }
  173. // Return course-module completion value
  174. return $cm->completion;
  175. }
  176. /**
  177. * Displays the 'Your progress' help icon, if completion tracking is enabled.
  178. * Just prints the result of display_help_icon().
  179. * @deprecated Use display_help_icon instead.
  180. * @return void
  181. */
  182. public function print_help_icon() {
  183. print $this->display_help_icon();
  184. }
  185. /**
  186. * Returns the 'Your progress' help icon, if completion tracking is enabled.
  187. * @global object
  188. * @return string HTML code for help icon, or blank if not needed
  189. */
  190. public function display_help_icon() {
  191. global $PAGE, $OUTPUT;
  192. $result = '';
  193. if ($this->is_enabled() && !$PAGE->user_is_editing() && isloggedin() && !isguestuser()) {
  194. $result .= '<span id = "completionprogressid" class="completionprogress">'.get_string('yourprogress','completion').' ';
  195. $result .= $OUTPUT->help_icon('completionicons', 'completion');
  196. $result .= '</span>';
  197. }
  198. return $result;
  199. }
  200. /**
  201. * Get a course completion for a user
  202. * @access public
  203. * @param $user_id int User id
  204. * @param $criteriatype int Specific criteria type to return
  205. * @return false|completion_criteria_completion
  206. */
  207. public function get_completion($user_id, $criteriatype) {
  208. $completions = $this->get_completions($user_id, $criteriatype);
  209. if (empty($completions)) {
  210. return false;
  211. } elseif (count($completions) > 1) {
  212. print_error('multipleselfcompletioncriteria', 'completion');
  213. }
  214. return $completions[0];
  215. }
  216. /**
  217. * Get all course criteria's completion objects for a user
  218. * @access public
  219. * @param $user_id int User id
  220. * @param $criteriatype int optional Specific criteria type to return
  221. * @return array
  222. */
  223. public function get_completions($user_id, $criteriatype = null) {
  224. $criterion = $this->get_criteria($criteriatype);
  225. $completions = array();
  226. foreach ($criterion as $criteria) {
  227. $params = array(
  228. 'course' => $this->course_id,
  229. 'userid' => $user_id,
  230. 'criteriaid' => $criteria->id
  231. );
  232. $completion = new completion_criteria_completion($params);
  233. $completion->attach_criteria($criteria);
  234. $completions[] = $completion;
  235. }
  236. return $completions;
  237. }
  238. /**
  239. * Get completion object for a user and a criteria
  240. * @access public
  241. * @param $user_id int User id
  242. * @param $criteria completion_criteria Criteria object
  243. * @return completion_criteria_completion
  244. */
  245. public function get_user_completion($user_id, $criteria) {
  246. $params = array(
  247. 'criteriaid' => $criteria->id,
  248. 'userid' => $user_id
  249. );
  250. $completion = new completion_criteria_completion($params);
  251. return $completion;
  252. }
  253. /**
  254. * Check if course has completion criteria set
  255. *
  256. * @access public
  257. * @return bool
  258. */
  259. public function has_criteria() {
  260. $criteria = $this->get_criteria();
  261. return (bool) count($criteria);
  262. }
  263. /**
  264. * Get course completion criteria
  265. * @access public
  266. * @param $criteriatype int optional Specific criteria type to return
  267. * @return void
  268. */
  269. public function get_criteria($criteriatype = null) {
  270. // Fill cache if empty
  271. if (!is_array($this->criteria)) {
  272. global $DB;
  273. $params = array(
  274. 'course' => $this->course->id
  275. );
  276. // Load criteria from database
  277. $records = (array)$DB->get_records('course_completion_criteria', $params);
  278. // Build array of criteria objects
  279. $this->criteria = array();
  280. foreach ($records as $record) {
  281. $this->criteria[$record->id] = completion_criteria::factory($record);
  282. }
  283. }
  284. // If after all criteria
  285. if ($criteriatype === null) {
  286. return $this->criteria;
  287. }
  288. // If we are only after a specific criteria type
  289. $criteria = array();
  290. foreach ($this->criteria as $criterion) {
  291. if ($criterion->criteriatype != $criteriatype) {
  292. continue;
  293. }
  294. $criteria[$criterion->id] = $criterion;
  295. }
  296. return $criteria;
  297. }
  298. /**
  299. * Get aggregation method
  300. * @access public
  301. * @param $criteriatype int optional If none supplied, get overall aggregation method
  302. * @return int
  303. */
  304. public function get_aggregation_method($criteriatype = null) {
  305. $params = array(
  306. 'course' => $this->course_id,
  307. 'criteriatype' => $criteriatype
  308. );
  309. $aggregation = new completion_aggregation($params);
  310. if (!$aggregation->id) {
  311. $aggregation->method = COMPLETION_AGGREGATION_ALL;
  312. }
  313. return $aggregation->method;
  314. }
  315. /**
  316. * Get incomplete course completion criteria
  317. * @access public
  318. * @return void
  319. */
  320. public function get_incomplete_criteria() {
  321. $incomplete = array();
  322. foreach ($this->get_criteria() as $criteria) {
  323. if (!$criteria->is_complete()) {
  324. $incomplete[] = $criteria;
  325. }
  326. }
  327. return $incomplete;
  328. }
  329. /**
  330. * Clear old course completion criteria
  331. */
  332. public function clear_criteria() {
  333. global $DB;
  334. $DB->delete_records('course_completion_criteria', array('course' => $this->course_id));
  335. $DB->delete_records('course_completion_aggr_methd', array('course' => $this->course_id));
  336. $this->delete_course_completion_data();
  337. }
  338. /**
  339. * Has the supplied user completed this course
  340. * @access public
  341. * @param $user_id int User's id
  342. * @return boolean
  343. */
  344. public function is_course_complete($user_id) {
  345. $params = array(
  346. 'userid' => $user_id,
  347. 'course' => $this->course_id
  348. );
  349. $ccompletion = new completion_completion($params);
  350. return $ccompletion->is_complete();
  351. }
  352. /**
  353. * Updates (if necessary) the completion state of activity $cm for the given
  354. * user.
  355. *
  356. * For manual completion, this function is called when completion is toggled
  357. * with $possibleresult set to the target state.
  358. *
  359. * For automatic completion, this function should be called every time a module
  360. * does something which might influence a user's completion state. For example,
  361. * if a forum provides options for marking itself 'completed' once a user makes
  362. * N posts, this function should be called every time a user makes a new post.
  363. * [After the post has been saved to the database]. When calling, you do not
  364. * need to pass in the new completion state. Instead this function carries out
  365. * completion calculation by checking grades and viewed state itself, and
  366. * calling the involved module via modulename_get_completion_state() to check
  367. * module-specific conditions.
  368. *
  369. * @global object
  370. * @global object
  371. * @uses COMPLETION_COMPLETE
  372. * @uses COMPLETION_INCOMPLETE
  373. * @uses COMPLETION_COMPLETE_PASS
  374. * @uses COMPLETION_COMPLETE_FAIL
  375. * @uses COMPLETION_TRACKING_MANUAL
  376. * @param object $cm Course-module
  377. * @param int $possibleresult Expected completion result. If the event that
  378. * has just occurred (e.g. add post) can only result in making the activity
  379. * complete when it wasn't before, use COMPLETION_COMPLETE. If the event that
  380. * has just occurred (e.g. delete post) can only result in making the activity
  381. * not complete when it was previously complete, use COMPLETION_INCOMPLETE.
  382. * Otherwise use COMPLETION_UNKNOWN. Setting this value to something other than
  383. * COMPLETION_UNKNOWN significantly improves performance because it will abandon
  384. * processing early if the user's completion state already matches the expected
  385. * result. For manual events, COMPLETION_COMPLETE or COMPLETION_INCOMPLETE
  386. * must be used; these directly set the specified state.
  387. * @param int $userid User ID to be updated. Default 0 = current user
  388. * @return void
  389. */
  390. public function update_state($cm, $possibleresult=COMPLETION_UNKNOWN, $userid=0) {
  391. global $USER, $SESSION;
  392. // Do nothing if completion is not enabled for that activity
  393. if (!$this->is_enabled($cm)) {
  394. return;
  395. }
  396. // Get current value of completion state and do nothing if it's same as
  397. // the possible result of this change. If the change is to COMPLETE and the
  398. // current value is one of the COMPLETE_xx subtypes, ignore that as well
  399. $current = $this->get_data($cm, false, $userid);
  400. if ($possibleresult == $current->completionstate ||
  401. ($possibleresult == COMPLETION_COMPLETE &&
  402. ($current->completionstate == COMPLETION_COMPLETE_PASS ||
  403. $current->completionstate == COMPLETION_COMPLETE_FAIL))) {
  404. return;
  405. }
  406. if ($cm->completion == COMPLETION_TRACKING_MANUAL) {
  407. // For manual tracking we set the result directly
  408. switch($possibleresult) {
  409. case COMPLETION_COMPLETE:
  410. case COMPLETION_INCOMPLETE:
  411. $newstate = $possibleresult;
  412. break;
  413. default:
  414. $this->internal_systemerror("Unexpected manual completion state for {$cm->id}: $possibleresult");
  415. }
  416. } else {
  417. // Automatic tracking; get new state
  418. $newstate = $this->internal_get_state($cm, $userid, $current);
  419. }
  420. // If changed, update
  421. if ($newstate != $current->completionstate) {
  422. $current->completionstate = $newstate;
  423. $current->timemodified = time();
  424. $this->internal_set_data($cm, $current);
  425. }
  426. }
  427. /**
  428. * Calculates the completion state for an activity and user.
  429. *
  430. * Internal function. Not private, so we can unit-test it.
  431. *
  432. * @global object
  433. * @global object
  434. * @global object
  435. * @uses COMPLETION_VIEW_REQUIRED
  436. * @uses COMPLETION_NOT_VIEWED
  437. * @uses COMPLETION_INCOMPLETE
  438. * @uses FEATURE_COMPLETION_HAS_RULES
  439. * @uses COMPLETION_COMPLETE
  440. * @uses COMPLETION_AND
  441. * @param object $cm Activity
  442. * @param int $userid ID of user
  443. * @param object $current Previous completion information from database
  444. * @return mixed
  445. */
  446. function internal_get_state($cm, $userid, $current) {
  447. global $USER, $DB, $CFG;
  448. // Get user ID
  449. if (!$userid) {
  450. $userid = $USER->id;
  451. }
  452. // Check viewed
  453. if ($cm->completionview == COMPLETION_VIEW_REQUIRED &&
  454. $current->viewed == COMPLETION_NOT_VIEWED) {
  455. return COMPLETION_INCOMPLETE;
  456. }
  457. // Modname hopefully is provided in $cm but just in case it isn't, let's grab it
  458. if (!isset($cm->modname)) {
  459. $cm->modname = $DB->get_field('modules', 'name', array('id'=>$cm->module));
  460. }
  461. $newstate = COMPLETION_COMPLETE;
  462. // Check grade
  463. if (!is_null($cm->completiongradeitemnumber)) {
  464. require_once($CFG->libdir.'/gradelib.php');
  465. $item = grade_item::fetch(array('courseid'=>$cm->course, 'itemtype'=>'mod',
  466. 'itemmodule'=>$cm->modname, 'iteminstance'=>$cm->instance,
  467. 'itemnumber'=>$cm->completiongradeitemnumber));
  468. if ($item) {
  469. // Fetch 'grades' (will be one or none)
  470. $grades = grade_grade::fetch_users_grades($item, array($userid), false);
  471. if (empty($grades)) {
  472. // No grade for user
  473. return COMPLETION_INCOMPLETE;
  474. }
  475. if (count($grades) > 1) {
  476. $this->internal_systemerror("Unexpected result: multiple grades for
  477. item '{$item->id}', user '{$userid}'");
  478. }
  479. $newstate = $this->internal_get_grade_state($item, reset($grades));
  480. if ($newstate == COMPLETION_INCOMPLETE) {
  481. return COMPLETION_INCOMPLETE;
  482. }
  483. } else {
  484. $this->internal_systemerror("Cannot find grade item for '{$cm->modname}'
  485. cm '{$cm->id}' matching number '{$cm->completiongradeitemnumber}'");
  486. }
  487. }
  488. if (plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_HAS_RULES)) {
  489. $function = $cm->modname.'_get_completion_state';
  490. if (!function_exists($function)) {
  491. $this->internal_systemerror("Module {$cm->modname} claims to support
  492. FEATURE_COMPLETION_HAS_RULES but does not have required
  493. {$cm->modname}_get_completion_state function");
  494. }
  495. if (!$function($this->course, $cm, $userid, COMPLETION_AND)) {
  496. return COMPLETION_INCOMPLETE;
  497. }
  498. }
  499. return $newstate;
  500. }
  501. /**
  502. * Marks a module as viewed.
  503. *
  504. * Should be called whenever a module is 'viewed' (it is up to the module how to
  505. * determine that). Has no effect if viewing is not set as a completion condition.
  506. *
  507. * @uses COMPLETION_VIEW_NOT_REQUIRED
  508. * @uses COMPLETION_VIEWED
  509. * @uses COMPLETION_COMPLETE
  510. * @param object $cm Activity
  511. * @param int $userid User ID or 0 (default) for current user
  512. * @return void
  513. */
  514. public function set_module_viewed($cm, $userid=0) {
  515. // Don't do anything if view condition is not turned on
  516. if ($cm->completionview == COMPLETION_VIEW_NOT_REQUIRED || !$this->is_enabled($cm)) {
  517. return;
  518. }
  519. // Get current completion state
  520. $data = $this->get_data($cm, $userid);
  521. // If we already viewed it, don't do anything
  522. if ($data->viewed == COMPLETION_VIEWED) {
  523. return;
  524. }
  525. // OK, change state, save it, and update completion
  526. $data->viewed = COMPLETION_VIEWED;
  527. $this->internal_set_data($cm, $data);
  528. $this->update_state($cm, COMPLETION_COMPLETE, $userid);
  529. }
  530. /**
  531. * Determines how much completion data exists for an activity. This is used when
  532. * deciding whether completion information should be 'locked' in the module
  533. * editing form.
  534. *
  535. * @global object
  536. * @param object $cm Activity
  537. * @return int The number of users who have completion data stored for this
  538. * activity, 0 if none
  539. */
  540. public function count_user_data($cm) {
  541. global $DB;
  542. return $DB->get_field_sql("
  543. SELECT
  544. COUNT(1)
  545. FROM
  546. {course_modules_completion}
  547. WHERE
  548. coursemoduleid=? AND completionstate<>0", array($cm->id));
  549. }
  550. /**
  551. * Determines how much course completion data exists for a course. This is used when
  552. * deciding whether completion information should be 'locked' in the completion
  553. * settings form and activity completion settings.
  554. *
  555. * @global object
  556. * @param int $user_id Optionally only get course completion data for a single user
  557. * @return int The number of users who have completion data stored for this
  558. * course, 0 if none
  559. */
  560. public function count_course_user_data($user_id = null) {
  561. global $DB;
  562. $sql = '
  563. SELECT
  564. COUNT(1)
  565. FROM
  566. {course_completion_crit_compl}
  567. WHERE
  568. course = ?
  569. ';
  570. $params = array($this->course_id);
  571. // Limit data to a single user if an ID is supplied
  572. if ($user_id) {
  573. $sql .= ' AND userid = ?';
  574. $params[] = $user_id;
  575. }
  576. return $DB->get_field_sql($sql, $params);
  577. }
  578. /**
  579. * Check if this course's completion criteria should be locked
  580. *
  581. * @return boolean
  582. */
  583. public function is_course_locked() {
  584. return (bool) $this->count_course_user_data();
  585. }
  586. /**
  587. * Deletes all course completion completion data.
  588. *
  589. * Intended to be used when unlocking completion criteria settings.
  590. *
  591. * @global object
  592. * @return void
  593. */
  594. public function delete_course_completion_data() {
  595. global $DB;
  596. $DB->delete_records('course_completions', array('course' => $this->course_id));
  597. $DB->delete_records('course_completion_crit_compl', array('course' => $this->course_id));
  598. }
  599. /**
  600. * Deletes completion state related to an activity for all users.
  601. *
  602. * Intended for use only when the activity itself is deleted.
  603. *
  604. * @global object
  605. * @global object
  606. * @param object $cm Activity
  607. */
  608. public function delete_all_state($cm) {
  609. global $SESSION, $DB;
  610. // Delete from database
  611. $DB->delete_records('course_modules_completion', array('coursemoduleid'=>$cm->id));
  612. // Erase cache data for current user if applicable
  613. if (isset($SESSION->completioncache) &&
  614. array_key_exists($cm->course, $SESSION->completioncache) &&
  615. array_key_exists($cm->id, $SESSION->completioncache[$cm->course])) {
  616. unset($SESSION->completioncache[$cm->course][$cm->id]);
  617. }
  618. // Check if there is an associated course completion criteria
  619. $criteria = $this->get_criteria(COMPLETION_CRITERIA_TYPE_ACTIVITY);
  620. $acriteria = false;
  621. foreach ($criteria as $criterion) {
  622. if ($criterion->moduleinstance == $cm->id) {
  623. $acriteria = $criterion;
  624. break;
  625. }
  626. }
  627. if ($acriteria) {
  628. // Delete all criteria completions relating to this activity
  629. $DB->delete_records('course_completion_crit_compl', array('course' => $this->course_id, 'criteriaid' => $acriteria->id));
  630. $DB->delete_records('course_completions', array('course' => $this->course_id));
  631. }
  632. }
  633. /**
  634. * Recalculates completion state related to an activity for all users.
  635. *
  636. * Intended for use if completion conditions change. (This should be avoided
  637. * as it may cause some things to become incomplete when they were previously
  638. * complete, with the effect - for example - of hiding a later activity that
  639. * was previously available.)
  640. *
  641. * Resetting state of manual tickbox has same result as deleting state for
  642. * it.
  643. *
  644. * @global object
  645. * @uses COMPLETION_TRACKING_MANUAL
  646. * @uses COMPLETION_UNKNOWN
  647. * @param object $cm Activity
  648. */
  649. public function reset_all_state($cm) {
  650. global $DB;
  651. if ($cm->completion == COMPLETION_TRACKING_MANUAL) {
  652. $this->delete_all_state($cm);
  653. return;
  654. }
  655. // Get current list of users with completion state
  656. $rs = $DB->get_recordset('course_modules_completion', array('coursemoduleid'=>$cm->id), '', 'userid');
  657. $keepusers = array();
  658. foreach ($rs as $rec) {
  659. $keepusers[] = $rec->userid;
  660. }
  661. $rs->close();
  662. // Delete all existing state [also clears session cache for current user]
  663. $this->delete_all_state($cm);
  664. // Merge this with list of planned users (according to roles)
  665. $trackedusers = $this->get_tracked_users();
  666. foreach ($trackedusers as $trackeduser) {
  667. $keepusers[] = $trackeduser->id;
  668. }
  669. $keepusers = array_unique($keepusers);
  670. // Recalculate state for each kept user
  671. foreach ($keepusers as $keepuser) {
  672. $this->update_state($cm, COMPLETION_UNKNOWN, $keepuser);
  673. }
  674. }
  675. /**
  676. * Obtains completion data for a particular activity and user (from the
  677. * session cache if available, or by SQL query)
  678. *
  679. * @global object
  680. * @global object
  681. * @global object
  682. * @global object
  683. * @uses COMPLETION_CACHE_EXPIRY
  684. * @param object $cm Activity; only required field is ->id
  685. * @param bool $wholecourse If true (default false) then, when necessary to
  686. * fill the cache, retrieves information from the entire course not just for
  687. * this one activity
  688. * @param int $userid User ID or 0 (default) for current user
  689. * @param array $modinfo Supply the value here - this is used for unit
  690. * testing and so that it can be called recursively from within
  691. * get_fast_modinfo. (Needs only list of all CMs with IDs.)
  692. * Otherwise the method calls get_fast_modinfo itself.
  693. * @return object Completion data (record from course_modules_completion)
  694. */
  695. public function get_data($cm, $wholecourse=false, $userid=0, $modinfo=null) {
  696. global $USER, $CFG, $SESSION, $DB;
  697. // Get user ID
  698. if (!$userid) {
  699. $userid = $USER->id;
  700. }
  701. // Is this the current user?
  702. $currentuser = $userid==$USER->id;
  703. if ($currentuser && is_object($SESSION)) {
  704. // Make sure cache is present and is for current user (loginas
  705. // changes this)
  706. if (!isset($SESSION->completioncache) || $SESSION->completioncacheuserid!=$USER->id) {
  707. $SESSION->completioncache = array();
  708. $SESSION->completioncacheuserid = $USER->id;
  709. }
  710. // Expire any old data from cache
  711. foreach ($SESSION->completioncache as $courseid=>$activities) {
  712. if (empty($activities['updated']) || $activities['updated'] < time()-COMPLETION_CACHE_EXPIRY) {
  713. unset($SESSION->completioncache[$courseid]);
  714. }
  715. }
  716. // See if requested data is present, if so use cache to get it
  717. if (isset($SESSION->completioncache) &&
  718. array_key_exists($this->course->id, $SESSION->completioncache) &&
  719. array_key_exists($cm->id, $SESSION->completioncache[$this->course->id])) {
  720. return $SESSION->completioncache[$this->course->id][$cm->id];
  721. }
  722. }
  723. // Not there, get via SQL
  724. if ($currentuser && $wholecourse) {
  725. // Get whole course data for cache
  726. $alldatabycmc = $DB->get_records_sql("
  727. SELECT
  728. cmc.*
  729. FROM
  730. {course_modules} cm
  731. INNER JOIN {course_modules_completion} cmc ON cmc.coursemoduleid=cm.id
  732. WHERE
  733. cm.course=? AND cmc.userid=?", array($this->course->id, $userid));
  734. // Reindex by cm id
  735. $alldata = array();
  736. if ($alldatabycmc) {
  737. foreach ($alldatabycmc as $data) {
  738. $alldata[$data->coursemoduleid] = $data;
  739. }
  740. }
  741. // Get the module info and build up condition info for each one
  742. if (empty($modinfo)) {
  743. $modinfo = get_fast_modinfo($this->course, $userid);
  744. }
  745. foreach ($modinfo->cms as $othercm) {
  746. if (array_key_exists($othercm->id, $alldata)) {
  747. $data = $alldata[$othercm->id];
  748. } else {
  749. // Row not present counts as 'not complete'
  750. $data = new StdClass;
  751. $data->id = 0;
  752. $data->coursemoduleid = $othercm->id;
  753. $data->userid = $userid;
  754. $data->completionstate = 0;
  755. $data->viewed = 0;
  756. $data->timemodified = 0;
  757. }
  758. $SESSION->completioncache[$this->course->id][$othercm->id] = $data;
  759. }
  760. $SESSION->completioncache[$this->course->id]['updated'] = time();
  761. if (!isset($SESSION->completioncache[$this->course->id][$cm->id])) {
  762. $this->internal_systemerror("Unexpected error: course-module {$cm->id} could not be found on course {$this->course->id}");
  763. }
  764. return $SESSION->completioncache[$this->course->id][$cm->id];
  765. } else {
  766. // Get single record
  767. $data = $DB->get_record('course_modules_completion', array('coursemoduleid'=>$cm->id, 'userid'=>$userid));
  768. if ($data == false) {
  769. // Row not present counts as 'not complete'
  770. $data = new StdClass;
  771. $data->id = 0;
  772. $data->coursemoduleid = $cm->id;
  773. $data->userid = $userid;
  774. $data->completionstate = 0;
  775. $data->viewed = 0;
  776. $data->timemodified = 0;
  777. }
  778. // Put in cache
  779. if ($currentuser) {
  780. $SESSION->completioncache[$this->course->id][$cm->id] = $data;
  781. // For single updates, only set date if it was empty before
  782. if (empty($SESSION->completioncache[$this->course->id]['updated'])) {
  783. $SESSION->completioncache[$this->course->id]['updated'] = time();
  784. }
  785. }
  786. }
  787. return $data;
  788. }
  789. /**
  790. * Updates completion data for a particular coursemodule and user (user is
  791. * determined from $data).
  792. *
  793. * (Internal function. Not private, so we can unit-test it.)
  794. *
  795. * @global object
  796. * @global object
  797. * @global object
  798. * @param object $cm Activity
  799. * @param object $data Data about completion for that user
  800. */
  801. function internal_set_data($cm, $data) {
  802. global $USER, $SESSION, $DB;
  803. if ($data->id) {
  804. // Has real (nonzero) id meaning that a database row exists
  805. $DB->update_record('course_modules_completion', $data);
  806. } else {
  807. // Didn't exist before, needs creating
  808. $data->id = $DB->insert_record('course_modules_completion', $data);
  809. }
  810. if ($data->userid == $USER->id) {
  811. $SESSION->completioncache[$cm->course][$cm->id] = $data;
  812. }
  813. }
  814. /**
  815. * Obtains a list of activities for which completion is enabled on the
  816. * course. The list is ordered by the section order of those activities.
  817. *
  818. * @global object
  819. * @uses COMPLETION_TRACKING_NONE
  820. * @param array $modinfo For unit testing only, supply the value
  821. * here. Otherwise the method calls get_fast_modinfo
  822. * @return array Array from $cmid => $cm of all activities with completion enabled,
  823. * empty array if none
  824. */
  825. public function get_activities($modinfo=null) {
  826. global $DB;
  827. // Obtain those activities which have completion turned on
  828. $withcompletion = $DB->get_records_select('course_modules', 'course='.$this->course->id.
  829. ' AND completion<>'.COMPLETION_TRACKING_NONE);
  830. if (!$withcompletion) {
  831. return array();
  832. }
  833. // Use modinfo to get section order and also add in names
  834. if (empty($modinfo)) {
  835. $modinfo = get_fast_modinfo($this->course);
  836. }
  837. $result = array();
  838. foreach ($modinfo->sections as $sectioncms) {
  839. foreach ($sectioncms as $cmid) {
  840. if (array_key_exists($cmid, $withcompletion)) {
  841. $result[$cmid] = $withcompletion[$cmid];
  842. $result[$cmid]->modname = $modinfo->cms[$cmid]->modname;
  843. $result[$cmid]->name = $modinfo->cms[$cmid]->name;
  844. }
  845. }
  846. }
  847. return $result;
  848. }
  849. /**
  850. * Checks to see if the userid supplied has a tracked role in
  851. * this course
  852. *
  853. * @param $userid User id
  854. * @return bool
  855. */
  856. function is_tracked_user($userid) {
  857. global $DB;
  858. $tracked = $this->generate_tracked_user_sql();
  859. $sql = "SELECT u.id ";
  860. $sql .= $tracked->sql;
  861. $sql .= ' AND u.id = :user';
  862. $params = $tracked->data;
  863. $params['user'] = (int)$userid;
  864. return $DB->record_exists_sql($sql, $params);
  865. }
  866. /**
  867. * Return number of users whose progress is tracked in this course
  868. *
  869. * Optionally supply a search's where clause, or a group id
  870. *
  871. * @param string $where Where clause sql
  872. * @param array $where_params Where clause params
  873. * @param int $groupid Group id
  874. * @return int
  875. */
  876. function get_num_tracked_users($where = '', $where_params = array(), $groupid = 0) {
  877. global $DB;
  878. $tracked = $this->generate_tracked_user_sql($groupid);
  879. $sql = "SELECT COUNT(u.id) ";
  880. $sql .= $tracked->sql;
  881. if ($where) {
  882. $sql .= " AND $where";
  883. }
  884. $params = array_merge($tracked->data, $where_params);
  885. return $DB->count_records_sql($sql, $params);
  886. }
  887. /**
  888. * Return array of users whose progress is tracked in this course
  889. *
  890. * Optionally supply a search's where caluse, group id, sorting, paging
  891. *
  892. * @param string $where Where clause sql (optional)
  893. * @param array $where_params Where clause params (optional)
  894. * @param integer $groupid Group ID to restrict to (optional)
  895. * @param string $sort Order by clause (optional)
  896. * @param integer $limitfrom Result start (optional)
  897. * @param integer $limitnum Result max size (optional)
  898. * @return array
  899. */
  900. function get_tracked_users($where = '', $where_params = array(), $groupid = 0,
  901. $sort = '', $limitfrom = '', $limitnum = '') {
  902. global $DB;
  903. $tracked = $this->generate_tracked_user_sql($groupid);
  904. $params = $tracked->data;
  905. $sql = "
  906. SELECT
  907. u.id,
  908. u.firstname,
  909. u.lastname,
  910. u.idnumber
  911. ";
  912. $sql .= $tracked->sql;
  913. if ($where) {
  914. $sql .= " AND $where";
  915. $params = array_merge($params, $where_params);
  916. }
  917. if ($sort) {
  918. $sql .= " ORDER BY $sort";
  919. }
  920. $users = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
  921. return $users ? $users : array(); // In case it returns false
  922. }
  923. /**
  924. * Generate the SQL for finding tracked users in this course
  925. *
  926. * Returns an object containing the sql fragment and an array of
  927. * bound data params.
  928. *
  929. * @param integer $groupid
  930. * @return object
  931. */
  932. function generate_tracked_user_sql($groupid = 0) {
  933. global $CFG;
  934. $return = new stdClass();
  935. $return->sql = '';
  936. $return->data = array();
  937. if (!empty($CFG->gradebookroles)) {
  938. $roles = ' AND ra.roleid IN ('.$CFG->gradebookroles.')';
  939. } else {
  940. // This causes it to default to everyone (if there is no student role)
  941. $roles = '';
  942. }
  943. // Build context sql
  944. $context = get_context_instance(CONTEXT_COURSE, $this->course->id);
  945. $parentcontexts = substr($context->path, 1); // kill leading slash
  946. $parentcontexts = str_replace('/', ',', $parentcontexts);
  947. if ($parentcontexts !== '') {
  948. $parentcontexts = ' OR ra.contextid IN ('.$parentcontexts.' )';
  949. }
  950. $groupjoin = '';
  951. $groupselect = '';
  952. if ($groupid) {
  953. $groupjoin = "JOIN {groups_members} gm
  954. ON gm.userid = u.id";
  955. $groupselect = " AND gm.groupid = :groupid ";
  956. $return->data['groupid'] = $groupid;
  957. }
  958. $return->sql = "
  959. FROM
  960. {user} u
  961. INNER JOIN
  962. {role_assignments} ra
  963. ON ra.userid = u.id
  964. INNER JOIN
  965. {role} r
  966. ON r.id = ra.roleid
  967. INNER JOIN
  968. {user_enrolments} ue
  969. ON ue.userid = u.id
  970. INNER JOIN
  971. {enrol} e
  972. ON e.id = ue.enrolid
  973. INNER JOIN
  974. {course} c
  975. ON c.id = e.courseid
  976. $groupjoin
  977. WHERE
  978. (ra.contextid = :contextid $parentcontexts)
  979. AND c.id = :courseid
  980. AND ue.status = 0
  981. AND e.status = 0
  982. AND ue.timestart < :now1
  983. AND (ue.timeend > :now2 OR ue.timeend = 0)
  984. $groupselect
  985. $roles
  986. ";
  987. $now = time();
  988. $return->data['now1'] = $now;
  989. $return->data['now2'] = $now;
  990. $return->data['contextid'] = $context->id;
  991. $return->data['courseid'] = $this->course->id;
  992. return $return;
  993. }
  994. /**
  995. * Obtains progress information across a course for all users on that course, or
  996. * for all users in a specific group. Intended for use when displaying progress.
  997. *
  998. * This includes only users who, in course context, have one of the roles for
  999. * which progress is tracked (the gradebookroles admin option) and are enrolled in course.
  1000. *
  1001. * Users are included (in the first array) even if they do not have
  1002. * completion progress for any course-module.
  1003. *
  1004. * @global object
  1005. * @global object
  1006. * @param bool $sortfirstname If true, sort by first name, otherwise sort by
  1007. * last name
  1008. * @param string $where Where clause sql (optional)
  1009. * @param array $where_params Where clause params (optional)
  1010. * @param int $groupid Group ID or 0 (default)/false for all groups
  1011. * @param int $pagesize Number of users to actually return (optional)
  1012. * @param int $start User to start at if paging (optional)
  1013. * @return Object with ->total and ->start (same as $start) and ->users;
  1014. * an array of user objects (like mdl_user id, firstname, lastname)
  1015. * containing an additional ->progress array of coursemoduleid => completionstate
  1016. */
  1017. public function get_progress_all($where = '', $where_params = array(), $groupid = 0,
  1018. $sort = '', $pagesize = '', $start = '') {
  1019. global $CFG, $DB;
  1020. // Get list of applicable users
  1021. $users = $this->get_tracked_users($where, $where_params, $groupid, $sort, $start, $pagesize);
  1022. // Get progress information for these users in groups of 1, 000 (if needed)
  1023. // to avoid making the SQL IN too long
  1024. $results = array();
  1025. $userids = array();
  1026. foreach ($users as $user) {
  1027. $userids[] = $user->id;
  1028. $results[$user->id] = $user;
  1029. $results[$user->id]->progress = array();
  1030. }
  1031. for($i=0; $i<count($userids); $i+=1000) {
  1032. $blocksize = count($userids)-$i < 1000 ? count($userids)-$i : 1000;
  1033. list($insql, $params) = $DB->get_in_or_equal(array_slice($userids, $i, $blocksize));
  1034. array_splice($params, 0, 0, array($this->course->id));
  1035. $rs = $DB->get_recordset_sql("
  1036. SELECT
  1037. cmc.*
  1038. FROM
  1039. {course_modules} cm
  1040. INNER JOIN {course_modules_completion} cmc ON cm.id=cmc.coursemoduleid
  1041. WHERE
  1042. cm.course=? AND cmc.userid $insql
  1043. ", $params);
  1044. foreach ($rs as $progress) {
  1045. $progress = (object)$progress;
  1046. $results[$progress->userid]->progress[$progress->coursemoduleid] = $progress;
  1047. }
  1048. $rs->close();
  1049. }
  1050. return $results;
  1051. }
  1052. /**
  1053. * Called by grade code to inform the completion system when a grade has
  1054. * been changed. If the changed grade is used to determine completion for
  1055. * the course-module, then the completion status will be updated.
  1056. *
  1057. * @uses COMPLETION_TRACKING_MANUAL
  1058. * @uses COMPLETION_INCOMPLETE
  1059. * @param object $cm Course-module for item that owns grade
  1060. * @param grade_item $item Grade item
  1061. * @param object $grade
  1062. * @param bool $deleted
  1063. * @return void
  1064. */
  1065. public function inform_grade_changed($cm, $item, $grade, $deleted) {
  1066. // Bail out now if completion is not enabled for course-module, it is enabled
  1067. // but is set to manual, grade is not used to compute completion, or this
  1068. // is a different numbered grade
  1069. if (!$this->is_enabled($cm) ||
  1070. $cm->completion == COMPLETION_TRACKING_MANUAL ||
  1071. is_null($cm->completiongradeitemnumber) ||
  1072. $item->itemnumber != $cm->completiongradeitemnumber) {
  1073. return;
  1074. }
  1075. // What is the expected result based on this grade?
  1076. if ($deleted) {
  1077. // Grade being deleted, so only change could be to make it incomplete
  1078. $possibleresult = COMPLETION_INCOMPLETE;
  1079. } else {
  1080. $possibleresult = $this->internal_get_grade_state($item, $grade);
  1081. }
  1082. // OK, let's update state based on this
  1083. $this->update_state($cm, $possibleresult, $grade->userid);
  1084. }
  1085. /**
  1086. * Calculates the completion state that would result from a graded item
  1087. * (where grade-based completion is turned on) based on the actual grade
  1088. * and settings.
  1089. *
  1090. * Internal function. Not private, so we can unit-test it.
  1091. *
  1092. * @uses COMPLETION_INCOMPLETE
  1093. * @uses COMPLETION_COMPLETE_PASS
  1094. * @uses COMPLETION_COMPLETE_FAIL
  1095. * @uses COMPLETION_COMPLETE
  1096. * @param object $item grade_item
  1097. * @param object $grade grade_grade
  1098. * @return int Completion state e.g. COMPLETION_INCOMPLETE
  1099. */
  1100. function internal_get_grade_state($item, $grade) {
  1101. if (!$grade) {
  1102. return COMPLETION_INCOMPLETE;
  1103. }
  1104. // Conditions to show pass/fail:
  1105. // a) Grade has pass mark (default is 0.00000 which is boolean true so be careful)
  1106. // b) Grade is visible (neither hidden nor hidden-until)
  1107. if ($item->gradepass && $item->gradepass > 0.000009 && !$item->hidden) {
  1108. // Use final grade if set otherwise raw grade
  1109. $score = !is_null($grade->finalgrade) ? $grade->finalgrade : $grade->rawgrade;
  1110. // We are displaying and tracking pass/fail
  1111. if ($score >= $item->gradepass) {
  1112. return COMPLETION_COMPLETE_PASS;
  1113. } else {
  1114. return COMPLETION_COMPLETE_FAIL;
  1115. }
  1116. } else {
  1117. // Not displaying pass/fail, so just if there is a grade
  1118. if (!is_null($grade->finalgrade) || !is_null($grade->rawgrade)) {
  1119. // Grade exists, so maybe complete now
  1120. return COMPLETION_COMPLETE;
  1121. } else {
  1122. // Grade does not exist, so maybe incomplete now
  1123. return COMPLETION_INCOMPLETE;
  1124. }
  1125. }
  1126. }
  1127. /**
  1128. * This is to be used only for system errors (things that shouldn't happen)
  1129. * and not user-level errors.
  1130. *
  1131. * @global object
  1132. * @param string $error Error string (will not be displayed to user unless
  1133. * debugging is enabled)
  1134. * @return void Throws moodle_exception Exception with the error string as debug info
  1135. */
  1136. function internal_systemerror($error) {
  1137. global $CFG;
  1138. throw new moodle_exception('err_system','completion',
  1139. $CFG->wwwroot.'/course/view.php?id='.$this->course->id,null,$error);
  1140. }
  1141. /**
  1142. * For testing only. Wipes information cached in user session.
  1143. *
  1144. * @global object
  1145. */
  1146. static function wipe_session_cache() {
  1147. global $SESSION;
  1148. unset($SESSION->completioncache);
  1149. unset($SESSION->completioncacheuserid);
  1150. }
  1151. }