Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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  /**
  18   * Contains classes, functions and constants used during the tracking
  19   * of activity completion for users.
  20   *
  21   * Completion top-level options (admin setting enablecompletion)
  22   *
  23   * @package core_completion
  24   * @category completion
  25   * @copyright 1999 onwards Martin Dougiamas   {@link http://moodle.com}
  26   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  
  29  use core_completion\activity_custom_completion;
  30  
  31  defined('MOODLE_INTERNAL') || die();
  32  
  33  /**
  34   * Include the required completion libraries
  35   */
  36  require_once $CFG->dirroot.'/completion/completion_aggregation.php';
  37  require_once $CFG->dirroot.'/completion/criteria/completion_criteria.php';
  38  require_once $CFG->dirroot.'/completion/completion_completion.php';
  39  require_once $CFG->dirroot.'/completion/completion_criteria_completion.php';
  40  
  41  
  42  /**
  43   * The completion system is enabled in this site/course
  44   */
  45  define('COMPLETION_ENABLED', 1);
  46  /**
  47   * The completion system is not enabled in this site/course
  48   */
  49  define('COMPLETION_DISABLED', 0);
  50  
  51  /**
  52   * Completion tracking is disabled for this activity
  53   * This is a completion tracking option per-activity  (course_modules/completion)
  54   */
  55  define('COMPLETION_TRACKING_NONE', 0);
  56  
  57  /**
  58   * Manual completion tracking (user ticks box) is enabled for this activity
  59   * This is a completion tracking option per-activity  (course_modules/completion)
  60   */
  61  define('COMPLETION_TRACKING_MANUAL', 1);
  62  /**
  63   * Automatic completion tracking (system ticks box) is enabled for this activity
  64   * This is a completion tracking option per-activity  (course_modules/completion)
  65   */
  66  define('COMPLETION_TRACKING_AUTOMATIC', 2);
  67  
  68  /**
  69   * The user has not completed this activity.
  70   * This is a completion state value (course_modules_completion/completionstate)
  71   */
  72  define('COMPLETION_INCOMPLETE', 0);
  73  /**
  74   * The user has completed this activity. It is not specified whether they have
  75   * passed or failed it.
  76   * This is a completion state value (course_modules_completion/completionstate)
  77   */
  78  define('COMPLETION_COMPLETE', 1);
  79  /**
  80   * The user has completed this activity with a grade above the pass mark.
  81   * This is a completion state value (course_modules_completion/completionstate)
  82   */
  83  define('COMPLETION_COMPLETE_PASS', 2);
  84  /**
  85   * The user has completed this activity but their grade is less than the pass mark
  86   * This is a completion state value (course_modules_completion/completionstate)
  87   */
  88  define('COMPLETION_COMPLETE_FAIL', 3);
  89  
  90  /**
  91   * The effect of this change to completion status is unknown.
  92   * A completion effect changes (used only in update_state)
  93   */
  94  define('COMPLETION_UNKNOWN', -1);
  95  /**
  96   * The user's grade has changed, so their new state might be
  97   * COMPLETION_COMPLETE_PASS or COMPLETION_COMPLETE_FAIL.
  98   * A completion effect changes (used only in update_state)
  99   */
 100  define('COMPLETION_GRADECHANGE', -2);
 101  
 102  /**
 103   * User must view this activity.
 104   * Whether view is required to create an activity (course_modules/completionview)
 105   */
 106  define('COMPLETION_VIEW_REQUIRED', 1);
 107  /**
 108   * User does not need to view this activity
 109   * Whether view is required to create an activity (course_modules/completionview)
 110   */
 111  define('COMPLETION_VIEW_NOT_REQUIRED', 0);
 112  
 113  /**
 114   * User has viewed this activity.
 115   * Completion viewed state (course_modules_completion/viewed)
 116   */
 117  define('COMPLETION_VIEWED', 1);
 118  /**
 119   * User has not viewed this activity.
 120   * Completion viewed state (course_modules_completion/viewed)
 121   */
 122  define('COMPLETION_NOT_VIEWED', 0);
 123  
 124  /**
 125   * Completion details should be ORed together and you should return false if
 126   * none apply.
 127   */
 128  define('COMPLETION_OR', false);
 129  /**
 130   * Completion details should be ANDed together and you should return true if
 131   * none apply
 132   */
 133  define('COMPLETION_AND', true);
 134  
 135  /**
 136   * Course completion criteria aggregation method.
 137   */
 138  define('COMPLETION_AGGREGATION_ALL', 1);
 139  /**
 140   * Course completion criteria aggregation method.
 141   */
 142  define('COMPLETION_AGGREGATION_ANY', 2);
 143  
 144  /**
 145   * Completion conditions will be displayed to user.
 146   */
 147  define('COMPLETION_SHOW_CONDITIONS', 1);
 148  
 149  /**
 150   * Completion conditions will be hidden from user.
 151   */
 152  define('COMPLETION_HIDE_CONDITIONS', 0);
 153  
 154  /**
 155   * Utility function for checking if the logged in user can view
 156   * another's completion data for a particular course
 157   *
 158   * @access  public
 159   * @param   int         $userid     Completion data's owner
 160   * @param   mixed       $course     Course object or Course ID (optional)
 161   * @return  boolean
 162   */
 163  function completion_can_view_data($userid, $course = null) {
 164      global $USER;
 165  
 166      if (!isloggedin()) {
 167          return false;
 168      }
 169  
 170      if (!is_object($course)) {
 171          $cid = $course;
 172          $course = new stdClass();
 173          $course->id = $cid;
 174      }
 175  
 176      // Check if this is the site course
 177      if ($course->id == SITEID) {
 178          $course = null;
 179      }
 180  
 181      // Check if completion is enabled
 182      if ($course) {
 183          $cinfo = new completion_info($course);
 184          if (!$cinfo->is_enabled()) {
 185              return false;
 186          }
 187      } else {
 188          if (!completion_info::is_enabled_for_site()) {
 189              return false;
 190          }
 191      }
 192  
 193      // Is own user's data?
 194      if ($USER->id == $userid) {
 195          return true;
 196      }
 197  
 198      // Check capabilities
 199      $personalcontext = context_user::instance($userid);
 200  
 201      if (has_capability('moodle/user:viewuseractivitiesreport', $personalcontext)) {
 202          return true;
 203      } elseif (has_capability('report/completion:view', $personalcontext)) {
 204          return true;
 205      }
 206  
 207      if ($course->id) {
 208          $coursecontext = context_course::instance($course->id);
 209      } else {
 210          $coursecontext = context_system::instance();
 211      }
 212  
 213      if (has_capability('report/completion:view', $coursecontext)) {
 214          return true;
 215      }
 216  
 217      return false;
 218  }
 219  
 220  
 221  /**
 222   * Class represents completion information for a course.
 223   *
 224   * Does not contain any data, so you can safely construct it multiple times
 225   * without causing any problems.
 226   *
 227   * @package core
 228   * @category completion
 229   * @copyright 2008 Sam Marshall
 230   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 231   */
 232  class completion_info {
 233  
 234      /* @var stdClass Course object passed during construction */
 235      private $course;
 236  
 237      /* @var int Course id */
 238      public $course_id;
 239  
 240      /* @var array Completion criteria {@link completion_info::get_criteria()}  */
 241      private $criteria;
 242  
 243      /**
 244       * Return array of aggregation methods
 245       * @return array
 246       */
 247      public static function get_aggregation_methods() {
 248          return array(
 249              COMPLETION_AGGREGATION_ALL => get_string('all'),
 250              COMPLETION_AGGREGATION_ANY => get_string('any', 'completion'),
 251          );
 252      }
 253  
 254      /**
 255       * Constructs with course details.
 256       *
 257       * When instantiating a new completion info object you must provide a course
 258       * object with at least id, and enablecompletion properties. Property
 259       * cacherev is needed if you check completion of the current user since
 260       * it is used for cache validation.
 261       *
 262       * @param stdClass $course Moodle course object.
 263       */
 264      public function __construct($course) {
 265          $this->course = $course;
 266          $this->course_id = $course->id;
 267      }
 268  
 269      /**
 270       * Determines whether completion is enabled across entire site.
 271       *
 272       * @return bool COMPLETION_ENABLED (true) if completion is enabled for the site,
 273       *     COMPLETION_DISABLED (false) if it's complete
 274       */
 275      public static function is_enabled_for_site() {
 276          global $CFG;
 277          return !empty($CFG->enablecompletion);
 278      }
 279  
 280      /**
 281       * Checks whether completion is enabled in a particular course and possibly
 282       * activity.
 283       *
 284       * @param stdClass|cm_info $cm Course-module object. If not specified, returns the course
 285       *   completion enable state.
 286       * @return mixed COMPLETION_ENABLED or COMPLETION_DISABLED (==0) in the case of
 287       *   site and course; COMPLETION_TRACKING_MANUAL, _AUTOMATIC or _NONE (==0)
 288       *   for a course-module.
 289       */
 290      public function is_enabled($cm = null) {
 291          global $CFG, $DB;
 292  
 293          // First check global completion
 294          if (!isset($CFG->enablecompletion) || $CFG->enablecompletion == COMPLETION_DISABLED) {
 295              return COMPLETION_DISABLED;
 296          }
 297  
 298          // Load data if we do not have enough
 299          if (!isset($this->course->enablecompletion)) {
 300              $this->course = get_course($this->course_id);
 301          }
 302  
 303          // Check course completion
 304          if ($this->course->enablecompletion == COMPLETION_DISABLED) {
 305              return COMPLETION_DISABLED;
 306          }
 307  
 308          // If there was no $cm and we got this far, then it's enabled
 309          if (!$cm) {
 310              return COMPLETION_ENABLED;
 311          }
 312  
 313          // Return course-module completion value
 314          return $cm->completion;
 315      }
 316  
 317      /**
 318       * Displays the 'Your progress' help icon, if completion tracking is enabled.
 319       * Just prints the result of display_help_icon().
 320       *
 321       * @deprecated since Moodle 2.0 - Use display_help_icon instead.
 322       */
 323      public function print_help_icon() {
 324          print $this->display_help_icon();
 325      }
 326  
 327      /**
 328       * Returns the 'Your progress' help icon, if completion tracking is enabled.
 329       *
 330       * @return string HTML code for help icon, or blank if not needed
 331       */
 332      public function display_help_icon() {
 333          global $PAGE, $OUTPUT, $USER;
 334          $result = '';
 335          if ($this->is_enabled() && !$PAGE->user_is_editing() && $this->is_tracked_user($USER->id) && isloggedin() &&
 336                  !isguestuser()) {
 337              $result .= html_writer::tag('div', get_string('yourprogress','completion') .
 338                      $OUTPUT->help_icon('completionicons', 'completion'), array('id' => 'completionprogressid',
 339                      'class' => 'completionprogress'));
 340          }
 341          return $result;
 342      }
 343  
 344      /**
 345       * Get a course completion for a user
 346       *
 347       * @param int $user_id User id
 348       * @param int $criteriatype Specific criteria type to return
 349       * @return bool|completion_criteria_completion returns false on fail
 350       */
 351      public function get_completion($user_id, $criteriatype) {
 352          $completions = $this->get_completions($user_id, $criteriatype);
 353  
 354          if (empty($completions)) {
 355              return false;
 356          } elseif (count($completions) > 1) {
 357              print_error('multipleselfcompletioncriteria', 'completion');
 358          }
 359  
 360          return $completions[0];
 361      }
 362  
 363      /**
 364       * Get all course criteria's completion objects for a user
 365       *
 366       * @param int $user_id User id
 367       * @param int $criteriatype Specific criteria type to return (optional)
 368       * @return array
 369       */
 370      public function get_completions($user_id, $criteriatype = null) {
 371          $criteria = $this->get_criteria($criteriatype);
 372  
 373          $completions = array();
 374  
 375          foreach ($criteria as $criterion) {
 376              $params = array(
 377                  'course'        => $this->course_id,
 378                  'userid'        => $user_id,
 379                  'criteriaid'    => $criterion->id
 380              );
 381  
 382              $completion = new completion_criteria_completion($params);
 383              $completion->attach_criteria($criterion);
 384  
 385              $completions[] = $completion;
 386          }
 387  
 388          return $completions;
 389      }
 390  
 391      /**
 392       * Get completion object for a user and a criteria
 393       *
 394       * @param int $user_id User id
 395       * @param completion_criteria $criteria Criteria object
 396       * @return completion_criteria_completion
 397       */
 398      public function get_user_completion($user_id, $criteria) {
 399          $params = array(
 400              'course'        => $this->course_id,
 401              'userid'        => $user_id,
 402              'criteriaid'    => $criteria->id,
 403          );
 404  
 405          $completion = new completion_criteria_completion($params);
 406          return $completion;
 407      }
 408  
 409      /**
 410       * Check if course has completion criteria set
 411       *
 412       * @return bool Returns true if there are criteria
 413       */
 414      public function has_criteria() {
 415          $criteria = $this->get_criteria();
 416  
 417          return (bool) count($criteria);
 418      }
 419  
 420      /**
 421       * Get course completion criteria
 422       *
 423       * @param int $criteriatype Specific criteria type to return (optional)
 424       */
 425      public function get_criteria($criteriatype = null) {
 426  
 427          // Fill cache if empty
 428          if (!is_array($this->criteria)) {
 429              global $DB;
 430  
 431              $params = array(
 432                  'course'    => $this->course->id
 433              );
 434  
 435              // Load criteria from database
 436              $records = (array)$DB->get_records('course_completion_criteria', $params);
 437  
 438              // Order records so activities are in the same order as they appear on the course view page.
 439              if ($records) {
 440                  $activitiesorder = array_keys(get_fast_modinfo($this->course)->get_cms());
 441                  usort($records, function ($a, $b) use ($activitiesorder) {
 442                      $aidx = ($a->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) ?
 443                          array_search($a->moduleinstance, $activitiesorder) : false;
 444                      $bidx = ($b->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) ?
 445                          array_search($b->moduleinstance, $activitiesorder) : false;
 446                      if ($aidx === false || $bidx === false || $aidx == $bidx) {
 447                          return 0;
 448                      }
 449                      return ($aidx < $bidx) ? -1 : 1;
 450                  });
 451              }
 452  
 453              // Build array of criteria objects
 454              $this->criteria = array();
 455              foreach ($records as $record) {
 456                  $this->criteria[$record->id] = completion_criteria::factory((array)$record);
 457              }
 458          }
 459  
 460          // If after all criteria
 461          if ($criteriatype === null) {
 462              return $this->criteria;
 463          }
 464  
 465          // If we are only after a specific criteria type
 466          $criteria = array();
 467          foreach ($this->criteria as $criterion) {
 468  
 469              if ($criterion->criteriatype != $criteriatype) {
 470                  continue;
 471              }
 472  
 473              $criteria[$criterion->id] = $criterion;
 474          }
 475  
 476          return $criteria;
 477      }
 478  
 479      /**
 480       * Get aggregation method
 481       *
 482       * @param int $criteriatype If none supplied, get overall aggregation method (optional)
 483       * @return int One of COMPLETION_AGGREGATION_ALL or COMPLETION_AGGREGATION_ANY
 484       */
 485      public function get_aggregation_method($criteriatype = null) {
 486          $params = array(
 487              'course'        => $this->course_id,
 488              'criteriatype'  => $criteriatype
 489          );
 490  
 491          $aggregation = new completion_aggregation($params);
 492  
 493          if (!$aggregation->id) {
 494              $aggregation->method = COMPLETION_AGGREGATION_ALL;
 495          }
 496  
 497          return $aggregation->method;
 498      }
 499  
 500      /**
 501       * @deprecated since Moodle 2.8 MDL-46290.
 502       */
 503      public function get_incomplete_criteria() {
 504          throw new coding_exception('completion_info->get_incomplete_criteria() is removed.');
 505      }
 506  
 507      /**
 508       * Clear old course completion criteria
 509       */
 510      public function clear_criteria() {
 511          global $DB;
 512  
 513          // Remove completion criteria records for the course itself, and any records that refer to the course.
 514          $select = 'course = :course OR (criteriatype = :type AND courseinstance = :courseinstance)';
 515          $params = [
 516              'course' => $this->course_id,
 517              'type' => COMPLETION_CRITERIA_TYPE_COURSE,
 518              'courseinstance' => $this->course_id,
 519          ];
 520  
 521          $DB->delete_records_select('course_completion_criteria', $select, $params);
 522          $DB->delete_records('course_completion_aggr_methd', array('course' => $this->course_id));
 523  
 524          $this->delete_course_completion_data();
 525      }
 526  
 527      /**
 528       * Has the supplied user completed this course
 529       *
 530       * @param int $user_id User's id
 531       * @return boolean
 532       */
 533      public function is_course_complete($user_id) {
 534          $params = array(
 535              'userid'    => $user_id,
 536              'course'  => $this->course_id
 537          );
 538  
 539          $ccompletion = new completion_completion($params);
 540          return $ccompletion->is_complete();
 541      }
 542  
 543      /**
 544       * Check whether the supplied user can override the activity completion statuses within the current course.
 545       *
 546       * @param stdClass $user The user object.
 547       * @return bool True if the user can override, false otherwise.
 548       */
 549      public function user_can_override_completion($user) {
 550          return has_capability('moodle/course:overridecompletion', context_course::instance($this->course_id), $user);
 551      }
 552  
 553      /**
 554       * Updates (if necessary) the completion state of activity $cm for the given
 555       * user.
 556       *
 557       * For manual completion, this function is called when completion is toggled
 558       * with $possibleresult set to the target state.
 559       *
 560       * For automatic completion, this function should be called every time a module
 561       * does something which might influence a user's completion state. For example,
 562       * if a forum provides options for marking itself 'completed' once a user makes
 563       * N posts, this function should be called every time a user makes a new post.
 564       * [After the post has been saved to the database]. When calling, you do not
 565       * need to pass in the new completion state. Instead this function carries out completion
 566       * calculation by checking grades and viewed state itself, and calling the involved module
 567       * via mod_{modulename}\\completion\\custom_completion::get_overall_completion_state() to
 568       * check module-specific conditions.
 569       *
 570       * @param stdClass|cm_info $cm Course-module
 571       * @param int $possibleresult Expected completion result. If the event that
 572       *   has just occurred (e.g. add post) can only result in making the activity
 573       *   complete when it wasn't before, use COMPLETION_COMPLETE. If the event that
 574       *   has just occurred (e.g. delete post) can only result in making the activity
 575       *   not complete when it was previously complete, use COMPLETION_INCOMPLETE.
 576       *   Otherwise use COMPLETION_UNKNOWN. Setting this value to something other than
 577       *   COMPLETION_UNKNOWN significantly improves performance because it will abandon
 578       *   processing early if the user's completion state already matches the expected
 579       *   result. For manual events, COMPLETION_COMPLETE or COMPLETION_INCOMPLETE
 580       *   must be used; these directly set the specified state.
 581       * @param int $userid User ID to be updated. Default 0 = current user
 582       * @param bool $override Whether manually overriding the existing completion state.
 583       * @return void
 584       * @throws moodle_exception if trying to override without permission.
 585       */
 586      public function update_state($cm, $possibleresult=COMPLETION_UNKNOWN, $userid=0, $override = false) {
 587          global $USER;
 588  
 589          // Do nothing if completion is not enabled for that activity
 590          if (!$this->is_enabled($cm)) {
 591              return;
 592          }
 593  
 594          // If we're processing an override and the current user isn't allowed to do so, then throw an exception.
 595          if ($override) {
 596              if (!$this->user_can_override_completion($USER)) {
 597                  throw new required_capability_exception(context_course::instance($this->course_id),
 598                                                          'moodle/course:overridecompletion', 'nopermission', '');
 599              }
 600          }
 601  
 602          // Default to current user if one is not provided.
 603          if ($userid == 0) {
 604              $userid = $USER->id;
 605          }
 606  
 607          // Delete the cm's cached completion data for this user if automatic completion is enabled.
 608          // This ensures any changes to the status of individual completion conditions in the activity will be fetched.
 609          if ($cm->completion == COMPLETION_TRACKING_AUTOMATIC) {
 610              $completioncache = cache::make('core', 'completion');
 611              $completionkey = $userid . '_' . $this->course->id;
 612              $completiondata = $completioncache->get($completionkey);
 613  
 614              if ($completiondata !== false) {
 615                  unset($completiondata[$cm->id]);
 616                  $completioncache->set($completionkey, $completiondata);
 617              }
 618          }
 619  
 620          // Get current value of completion state and do nothing if it's same as
 621          // the possible result of this change. If the change is to COMPLETE and the
 622          // current value is one of the COMPLETE_xx subtypes, ignore that as well
 623          $current = $this->get_data($cm, false, $userid);
 624          if ($possibleresult == $current->completionstate ||
 625              ($possibleresult == COMPLETION_COMPLETE &&
 626                  ($current->completionstate == COMPLETION_COMPLETE_PASS ||
 627                  $current->completionstate == COMPLETION_COMPLETE_FAIL))) {
 628              return;
 629          }
 630  
 631          // For auto tracking, if the status is overridden to 'COMPLETION_COMPLETE', then disallow further changes,
 632          // unless processing another override.
 633          // Basically, we want those activities which have been overridden to COMPLETE to hold state, and those which have been
 634          // overridden to INCOMPLETE to still be processed by normal completion triggers.
 635          if ($cm->completion == COMPLETION_TRACKING_AUTOMATIC && !is_null($current->overrideby)
 636              && $current->completionstate == COMPLETION_COMPLETE && !$override) {
 637              return;
 638          }
 639  
 640          // For manual tracking, or if overriding the completion state, we set the state directly.
 641          if ($cm->completion == COMPLETION_TRACKING_MANUAL || $override) {
 642              switch($possibleresult) {
 643                  case COMPLETION_COMPLETE:
 644                  case COMPLETION_INCOMPLETE:
 645                      $newstate = $possibleresult;
 646                      break;
 647                  default:
 648                      $this->internal_systemerror("Unexpected manual completion state for {$cm->id}: $possibleresult");
 649              }
 650  
 651          } else {
 652              $newstate = $this->internal_get_state($cm, $userid, $current);
 653          }
 654  
 655          // If the overall completion state has changed, update it in the cache.
 656          if ($newstate != $current->completionstate) {
 657              $current->completionstate = $newstate;
 658              $current->timemodified    = time();
 659              $current->overrideby      = $override ? $USER->id : null;
 660              $this->internal_set_data($cm, $current);
 661          }
 662      }
 663  
 664      /**
 665       * Calculates the completion state for an activity and user.
 666       *
 667       * Internal function. Not private, so we can unit-test it.
 668       *
 669       * @param stdClass|cm_info $cm Activity
 670       * @param int $userid ID of user
 671       * @param stdClass $current Previous completion information from database
 672       * @return mixed
 673       */
 674      public function internal_get_state($cm, $userid, $current) {
 675          global $USER, $DB;
 676  
 677          // Get user ID
 678          if (!$userid) {
 679              $userid = $USER->id;
 680          }
 681  
 682          // Check viewed
 683          if ($cm->completionview == COMPLETION_VIEW_REQUIRED &&
 684              $current->viewed == COMPLETION_NOT_VIEWED) {
 685  
 686              return COMPLETION_INCOMPLETE;
 687          }
 688  
 689          if ($cm instanceof stdClass) {
 690              // Modname hopefully is provided in $cm but just in case it isn't, let's grab it.
 691              if (!isset($cm->modname)) {
 692                  $cm->modname = $DB->get_field('modules', 'name', array('id' => $cm->module));
 693              }
 694              // Some functions call this method and pass $cm as an object with ID only. Make sure course is set as well.
 695              if (!isset($cm->course)) {
 696                  $cm->course = $this->course_id;
 697              }
 698          }
 699          // Make sure we're using a cm_info object.
 700          $cminfo = cm_info::create($cm, $userid);
 701  
 702          $newstate = COMPLETION_COMPLETE;
 703  
 704          // Check grade
 705          if (!is_null($cminfo->completiongradeitemnumber)) {
 706              $newstate = $this->get_grade_completion($cminfo, $userid);
 707              if ($newstate == COMPLETION_INCOMPLETE) {
 708                  return COMPLETION_INCOMPLETE;
 709              }
 710          }
 711  
 712          if (plugin_supports('mod', $cminfo->modname, FEATURE_COMPLETION_HAS_RULES)) {
 713              $cmcompletionclass = activity_custom_completion::get_cm_completion_class($cminfo->modname);
 714              if ($cmcompletionclass) {
 715                  /** @var activity_custom_completion $cmcompletion */
 716                  $cmcompletion = new $cmcompletionclass($cminfo, $userid);
 717                  if ($cmcompletion->get_overall_completion_state() == COMPLETION_INCOMPLETE) {
 718                      return COMPLETION_INCOMPLETE;
 719                  }
 720              } else {
 721                  // Fallback to the get_completion_state callback.
 722                  $cmcompletionclass = "mod_{$cminfo->modname}\\completion\\custom_completion";
 723                  $function = $cminfo->modname . '_get_completion_state';
 724                  if (!function_exists($function)) {
 725                      $this->internal_systemerror("Module {$cminfo->modname} claims to support FEATURE_COMPLETION_HAS_RULES " .
 726                          "but does not implement the custom completion class $cmcompletionclass which extends " .
 727                          "\core_completion\activity_custom_completion.");
 728                  }
 729                  debugging("*_get_completion_state() callback functions such as $function have been deprecated and should no " .
 730                      "longer be used. Please implement the custom completion class $cmcompletionclass which extends " .
 731                      "\core_completion\activity_custom_completion.", DEBUG_DEVELOPER);
 732                  if (!$function($this->course, $cminfo, $userid, COMPLETION_AND)) {
 733                      return COMPLETION_INCOMPLETE;
 734                  }
 735              }
 736          }
 737  
 738          return $newstate;
 739  
 740      }
 741  
 742      /**
 743       * Fetches the completion state for an activity completion's require grade completion requirement.
 744       *
 745       * @param cm_info $cm The course module information.
 746       * @param int $userid The user ID.
 747       * @return int The completion state.
 748       */
 749      public function get_grade_completion(cm_info $cm, int $userid): int {
 750          global $CFG;
 751  
 752          require_once($CFG->libdir . '/gradelib.php');
 753          $item = grade_item::fetch([
 754              'courseid' => $cm->course,
 755              'itemtype' => 'mod',
 756              'itemmodule' => $cm->modname,
 757              'iteminstance' => $cm->instance,
 758              'itemnumber' => $cm->completiongradeitemnumber
 759          ]);
 760          if ($item) {
 761              // Fetch 'grades' (will be one or none).
 762              $grades = grade_grade::fetch_users_grades($item, [$userid], false);
 763              if (empty($grades)) {
 764                  // No grade for user.
 765                  return COMPLETION_INCOMPLETE;
 766              }
 767              if (count($grades) > 1) {
 768                  $this->internal_systemerror("Unexpected result: multiple grades for
 769                          item '{$item->id}', user '{$userid}'");
 770              }
 771              return self::internal_get_grade_state($item, reset($grades));
 772          }
 773  
 774          return COMPLETION_INCOMPLETE;
 775      }
 776  
 777      /**
 778       * Marks a module as viewed.
 779       *
 780       * Should be called whenever a module is 'viewed' (it is up to the module how to
 781       * determine that). Has no effect if viewing is not set as a completion condition.
 782       *
 783       * Note that this function must be called before you print the page header because
 784       * it is possible that the navigation block may depend on it. If you call it after
 785       * printing the header, it shows a developer debug warning.
 786       *
 787       * @param stdClass|cm_info $cm Activity
 788       * @param int $userid User ID or 0 (default) for current user
 789       * @return void
 790       */
 791      public function set_module_viewed($cm, $userid=0) {
 792          global $PAGE;
 793          if ($PAGE->headerprinted) {
 794              debugging('set_module_viewed must be called before header is printed',
 795                      DEBUG_DEVELOPER);
 796          }
 797  
 798          // Don't do anything if view condition is not turned on
 799          if ($cm->completionview == COMPLETION_VIEW_NOT_REQUIRED || !$this->is_enabled($cm)) {
 800              return;
 801          }
 802  
 803          // Get current completion state
 804          $data = $this->get_data($cm, false, $userid);
 805  
 806          // If we already viewed it, don't do anything unless the completion status is overridden.
 807          // If the completion status is overridden, then we need to allow this 'view' to trigger automatic completion again.
 808          if ($data->viewed == COMPLETION_VIEWED && empty($data->overrideby)) {
 809              return;
 810          }
 811  
 812          // OK, change state, save it, and update completion
 813          $data->viewed = COMPLETION_VIEWED;
 814          $this->internal_set_data($cm, $data);
 815          $this->update_state($cm, COMPLETION_COMPLETE, $userid);
 816      }
 817  
 818      /**
 819       * Determines how much completion data exists for an activity. This is used when
 820       * deciding whether completion information should be 'locked' in the module
 821       * editing form.
 822       *
 823       * @param cm_info $cm Activity
 824       * @return int The number of users who have completion data stored for this
 825       *   activity, 0 if none
 826       */
 827      public function count_user_data($cm) {
 828          global $DB;
 829  
 830          return $DB->get_field_sql("
 831      SELECT
 832          COUNT(1)
 833      FROM
 834          {course_modules_completion}
 835      WHERE
 836          coursemoduleid=? AND completionstate<>0", array($cm->id));
 837      }
 838  
 839      /**
 840       * Determines how much course completion data exists for a course. This is used when
 841       * deciding whether completion information should be 'locked' in the completion
 842       * settings form and activity completion settings.
 843       *
 844       * @param int $user_id Optionally only get course completion data for a single user
 845       * @return int The number of users who have completion data stored for this
 846       *     course, 0 if none
 847       */
 848      public function count_course_user_data($user_id = null) {
 849          global $DB;
 850  
 851          $sql = '
 852      SELECT
 853          COUNT(1)
 854      FROM
 855          {course_completion_crit_compl}
 856      WHERE
 857          course = ?
 858          ';
 859  
 860          $params = array($this->course_id);
 861  
 862          // Limit data to a single user if an ID is supplied
 863          if ($user_id) {
 864              $sql .= ' AND userid = ?';
 865              $params[] = $user_id;
 866          }
 867  
 868          return $DB->get_field_sql($sql, $params);
 869      }
 870  
 871      /**
 872       * Check if this course's completion criteria should be locked
 873       *
 874       * @return boolean
 875       */
 876      public function is_course_locked() {
 877          return (bool) $this->count_course_user_data();
 878      }
 879  
 880      /**
 881       * Deletes all course completion completion data.
 882       *
 883       * Intended to be used when unlocking completion criteria settings.
 884       */
 885      public function delete_course_completion_data() {
 886          global $DB;
 887  
 888          $DB->delete_records('course_completions', array('course' => $this->course_id));
 889          $DB->delete_records('course_completion_crit_compl', array('course' => $this->course_id));
 890  
 891          // Difficult to find affected users, just purge all completion cache.
 892          cache::make('core', 'completion')->purge();
 893          cache::make('core', 'coursecompletion')->purge();
 894      }
 895  
 896      /**
 897       * Deletes all activity and course completion data for an entire course
 898       * (the below delete_all_state function does this for a single activity).
 899       *
 900       * Used by course reset page.
 901       */
 902      public function delete_all_completion_data() {
 903          global $DB;
 904  
 905          // Delete from database.
 906          $DB->delete_records_select('course_modules_completion',
 907                  'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=?)',
 908                  array($this->course_id));
 909  
 910          // Wipe course completion data too.
 911          $this->delete_course_completion_data();
 912      }
 913  
 914      /**
 915       * Deletes completion state related to an activity for all users.
 916       *
 917       * Intended for use only when the activity itself is deleted.
 918       *
 919       * @param stdClass|cm_info $cm Activity
 920       */
 921      public function delete_all_state($cm) {
 922          global $DB;
 923  
 924          // Delete from database
 925          $DB->delete_records('course_modules_completion', array('coursemoduleid'=>$cm->id));
 926  
 927          // Check if there is an associated course completion criteria
 928          $criteria = $this->get_criteria(COMPLETION_CRITERIA_TYPE_ACTIVITY);
 929          $acriteria = false;
 930          foreach ($criteria as $criterion) {
 931              if ($criterion->moduleinstance == $cm->id) {
 932                  $acriteria = $criterion;
 933                  break;
 934              }
 935          }
 936  
 937          if ($acriteria) {
 938              // Delete all criteria completions relating to this activity
 939              $DB->delete_records('course_completion_crit_compl', array('course' => $this->course_id, 'criteriaid' => $acriteria->id));
 940              $DB->delete_records('course_completions', array('course' => $this->course_id));
 941          }
 942  
 943          // Difficult to find affected users, just purge all completion cache.
 944          cache::make('core', 'completion')->purge();
 945          cache::make('core', 'coursecompletion')->purge();
 946      }
 947  
 948      /**
 949       * Recalculates completion state related to an activity for all users.
 950       *
 951       * Intended for use if completion conditions change. (This should be avoided
 952       * as it may cause some things to become incomplete when they were previously
 953       * complete, with the effect - for example - of hiding a later activity that
 954       * was previously available.)
 955       *
 956       * Resetting state of manual tickbox has same result as deleting state for
 957       * it.
 958       *
 959       * @param stcClass|cm_info $cm Activity
 960       */
 961      public function reset_all_state($cm) {
 962          global $DB;
 963  
 964          if ($cm->completion == COMPLETION_TRACKING_MANUAL) {
 965              $this->delete_all_state($cm);
 966              return;
 967          }
 968          // Get current list of users with completion state
 969          $rs = $DB->get_recordset('course_modules_completion', array('coursemoduleid'=>$cm->id), '', 'userid');
 970          $keepusers = array();
 971          foreach ($rs as $rec) {
 972              $keepusers[] = $rec->userid;
 973          }
 974          $rs->close();
 975  
 976          // Delete all existing state.
 977          $this->delete_all_state($cm);
 978  
 979          // Merge this with list of planned users (according to roles)
 980          $trackedusers = $this->get_tracked_users();
 981          foreach ($trackedusers as $trackeduser) {
 982              $keepusers[] = $trackeduser->id;
 983          }
 984          $keepusers = array_unique($keepusers);
 985  
 986          // Recalculate state for each kept user
 987          foreach ($keepusers as $keepuser) {
 988              $this->update_state($cm, COMPLETION_UNKNOWN, $keepuser);
 989          }
 990      }
 991  
 992      /**
 993       * Obtains completion data for a particular activity and user (from the
 994       * completion cache if available, or by SQL query)
 995       *
 996       * @param stdClass|cm_info $cm Activity; only required field is ->id
 997       * @param bool $wholecourse If true (default false) then, when necessary to
 998       *   fill the cache, retrieves information from the entire course not just for
 999       *   this one activity
1000       * @param int $userid User ID or 0 (default) for current user
1001       * @param array $modinfo Supply the value here - this is used for unit
1002       *   testing and so that it can be called recursively from within
1003       *   get_fast_modinfo. (Needs only list of all CMs with IDs.)
1004       *   Otherwise the method calls get_fast_modinfo itself.
1005       * @return object Completion data. Record from course_modules_completion plus other completion statuses such as
1006       *                  - Completion status for 'must-receive-grade' completion rule.
1007       *                  - Custom completion statuses defined by the activity module plugin.
1008       */
1009      public function get_data($cm, $wholecourse = false, $userid = 0, $modinfo = null) {
1010          global $USER, $DB;
1011          $completioncache = cache::make('core', 'completion');
1012  
1013          // Get user ID
1014          if (!$userid) {
1015              $userid = $USER->id;
1016          }
1017  
1018          // Some call completion_info::get_data and pass $cm as an object with ID only. Make sure course is set as well.
1019          if ($cm instanceof stdClass && !isset($cm->course)) {
1020              $cm->course = $this->course_id;
1021          }
1022          // Make sure we're working on a cm_info object.
1023          $cminfo = cm_info::create($cm, $userid);
1024  
1025          // Create an anonymous function to remove the 'other_cm_completion_data_fetched' key.
1026          $returnfilteredvalue = function(array $value): stdClass {
1027              return (object) array_filter($value, function(string $key): bool {
1028                  return $key !== 'other_cm_completion_data_fetched';
1029              }, ARRAY_FILTER_USE_KEY);
1030          };
1031  
1032          // See if requested data is present in cache (use cache for current user only).
1033          $usecache = $userid == $USER->id;
1034          $cacheddata = array();
1035          if ($usecache) {
1036              $key = $userid . '_' . $this->course->id;
1037              if (!isset($this->course->cacherev)) {
1038                  $this->course = get_course($this->course_id);
1039              }
1040              if ($cacheddata = $completioncache->get($key)) {
1041                  if ($cacheddata['cacherev'] != $this->course->cacherev) {
1042                      // Course structure has been changed since the last caching, forget the cache.
1043                      $cacheddata = array();
1044                  } else if (isset($cacheddata[$cminfo->id])) {
1045                      $data = (array) $cacheddata[$cminfo->id];
1046                      if (empty($data['other_cm_completion_data_fetched'])) {
1047                          $data += $this->get_other_cm_completion_data($cminfo, $userid);
1048                          $data['other_cm_completion_data_fetched'] = true;
1049  
1050                          // Put in cache.
1051                          $cacheddata[$cminfo->id] = $data;
1052                          $completioncache->set($key, $cacheddata);
1053                      }
1054  
1055                      return $returnfilteredvalue($cacheddata[$cminfo->id]);
1056                  }
1057              }
1058          }
1059  
1060          // Default data to return when no completion data is found.
1061          $defaultdata = [
1062              'id' => 0,
1063              'coursemoduleid' => $cminfo->id,
1064              'userid' => $userid,
1065              'completionstate' => 0,
1066              'viewed' => 0,
1067              'overrideby' => null,
1068              'timemodified' => 0,
1069          ];
1070  
1071          // If cached completion data is not found, fetch via SQL.
1072          // Fetch completion data for all of the activities in the course ONLY if we're caching the fetched completion data.
1073          // If we're not caching the completion data, then just fetch the completion data for the user in this course module.
1074          if ($usecache && $wholecourse) {
1075              // Get whole course data for cache.
1076              $alldatabycmc = $DB->get_records_sql("SELECT cm.id AS cmid, cmc.*
1077                                                      FROM {course_modules} cm
1078                                                 LEFT JOIN {course_modules_completion} cmc ON cmc.coursemoduleid = cm.id
1079                                                           AND cmc.userid = ?
1080                                                INNER JOIN {modules} m ON m.id = cm.module
1081                                                     WHERE m.visible = 1 AND cm.course = ?", [$userid, $this->course->id]);
1082  
1083              $cminfos = get_fast_modinfo($cm->course, $userid)->get_cms();
1084  
1085              // Reindex by course module id.
1086              foreach ($alldatabycmc as $data) {
1087  
1088                  // Filter acitivites with no cm_info (missing plugins or other causes).
1089                  if (!isset($cminfos[$data->cmid])) {
1090                      continue;
1091                  }
1092  
1093                  if (empty($data->coursemoduleid)) {
1094                      $cacheddata[$data->cmid] = $defaultdata;
1095                      $cacheddata[$data->cmid]['coursemoduleid'] = $data->cmid;
1096                  } else {
1097                      unset($data->cmid);
1098                      $cacheddata[$data->coursemoduleid] = (array) $data;
1099                  }
1100              }
1101  
1102              if (!isset($cacheddata[$cminfo->id])) {
1103                  $errormessage = "Unexpected error: course-module {$cminfo->id} could not be found on course {$this->course->id}";
1104                  $this->internal_systemerror($errormessage);
1105              }
1106  
1107              $data = $cacheddata[$cminfo->id];
1108          } else {
1109              // Get single record
1110              $data = $DB->get_record('course_modules_completion', array('coursemoduleid' => $cminfo->id, 'userid' => $userid));
1111              if ($data) {
1112                  $data = (array)$data;
1113              } else {
1114                  // Row not present counts as 'not complete'.
1115                  $data = $defaultdata;
1116              }
1117  
1118              // Put in cache.
1119              $cacheddata[$cminfo->id] = $data;
1120          }
1121  
1122          // Fill the other completion data for this user in this module instance.
1123          $data += $this->get_other_cm_completion_data($cminfo, $userid);
1124          $data['other_cm_completion_data_fetched'] = true;
1125  
1126          // Put in cache
1127          $cacheddata[$cminfo->id] = $data;
1128  
1129          if ($usecache) {
1130              $cacheddata['cacherev'] = $this->course->cacherev;
1131              $completioncache->set($key, $cacheddata);
1132          }
1133  
1134          return $returnfilteredvalue($cacheddata[$cminfo->id]);
1135      }
1136  
1137      /**
1138       * Adds the user's custom completion data on the given course module.
1139       *
1140       * @param cm_info $cm The course module information.
1141       * @param int $userid The user ID.
1142       * @return array The additional completion data.
1143       */
1144      protected function get_other_cm_completion_data(cm_info $cm, int $userid): array {
1145          $data = [];
1146  
1147          // Include in the completion info the grade completion, if necessary.
1148          if (!is_null($cm->completiongradeitemnumber)) {
1149              $data['completiongrade'] = $this->get_grade_completion($cm, $userid);
1150          }
1151  
1152          // Custom activity module completion data.
1153  
1154          // Cast custom data to array before checking for custom completion rules.
1155          // We call ->get_custom_data() instead of ->customdata here because there is the chance of recursive calling,
1156          // and we cannot call a getter from a getter in PHP.
1157          $customdata = (array) $cm->get_custom_data();
1158          // Return early if the plugin does not define custom completion rules.
1159          if (empty($customdata['customcompletionrules'])) {
1160              return $data;
1161          }
1162  
1163          // Return early if the activity modules doe not implement the activity_custom_completion class.
1164          $cmcompletionclass = activity_custom_completion::get_cm_completion_class($cm->modname);
1165          if (!$cmcompletionclass) {
1166              return $data;
1167          }
1168  
1169          /** @var activity_custom_completion $customcmcompletion */
1170          $customcmcompletion = new $cmcompletionclass($cm, $userid);
1171          foreach ($customdata['customcompletionrules'] as $rule => $enabled) {
1172              if (!$enabled) {
1173                  // Skip inactive completion rules.
1174                  continue;
1175              }
1176              // Get this custom completion rule's completion state.
1177              $data['customcompletion'][$rule] = $customcmcompletion->get_state($rule);
1178          }
1179  
1180          return $data;
1181      }
1182  
1183      /**
1184       * Updates completion data for a particular coursemodule and user (user is
1185       * determined from $data).
1186       *
1187       * (Internal function. Not private, so we can unit-test it.)
1188       *
1189       * @param stdClass|cm_info $cm Activity
1190       * @param stdClass $data Data about completion for that user
1191       */
1192      public function internal_set_data($cm, $data) {
1193          global $USER, $DB;
1194  
1195          $transaction = $DB->start_delegated_transaction();
1196          if (!$data->id) {
1197              // Check there isn't really a row
1198              $data->id = $DB->get_field('course_modules_completion', 'id',
1199                      array('coursemoduleid'=>$data->coursemoduleid, 'userid'=>$data->userid));
1200          }
1201          if (!$data->id) {
1202              // Didn't exist before, needs creating
1203              $data->id = $DB->insert_record('course_modules_completion', $data);
1204          } else {
1205              // Has real (nonzero) id meaning that a database row exists, update
1206              $DB->update_record('course_modules_completion', $data);
1207          }
1208          $transaction->allow_commit();
1209  
1210          $cmcontext = context_module::instance($data->coursemoduleid);
1211  
1212          $completioncache = cache::make('core', 'completion');
1213          $cachekey = "{$data->userid}_{$cm->course}";
1214          if ($data->userid == $USER->id) {
1215              // Fetch other completion data to cache (e.g. require grade completion status, custom completion rule statues).
1216              $cminfo = cm_info::create($cm, $data->userid); // Make sure we're working on a cm_info object.
1217              $otherdata = $this->get_other_cm_completion_data($cminfo, $data->userid);
1218              foreach ($otherdata as $key => $value) {
1219                  $data->$key = $value;
1220              }
1221  
1222              // Update module completion in user's cache.
1223              if (!($cachedata = $completioncache->get($cachekey))
1224                      || $cachedata['cacherev'] != $this->course->cacherev) {
1225                  $cachedata = array('cacherev' => $this->course->cacherev);
1226              }
1227              $cachedata[$cm->id] = (array) $data;
1228              $cachedata[$cm->id]['other_cm_completion_data_fetched'] = true;
1229              $completioncache->set($cachekey, $cachedata);
1230  
1231              // reset modinfo for user (no need to call rebuild_course_cache())
1232              get_fast_modinfo($cm->course, 0, true);
1233          } else {
1234              // Remove another user's completion cache for this course.
1235              $completioncache->delete($cachekey);
1236          }
1237  
1238          // Trigger an event for course module completion changed.
1239          $event = \core\event\course_module_completion_updated::create(array(
1240              'objectid' => $data->id,
1241              'context' => $cmcontext,
1242              'relateduserid' => $data->userid,
1243              'other' => array(
1244                  'relateduserid' => $data->userid,
1245                  'overrideby' => $data->overrideby,
1246                  'completionstate' => $data->completionstate
1247              )
1248          ));
1249          $event->add_record_snapshot('course_modules_completion', $data);
1250          $event->trigger();
1251      }
1252  
1253       /**
1254       * Return whether or not the course has activities with completion enabled.
1255       *
1256       * @return boolean true when there is at least one activity with completion enabled.
1257       */
1258      public function has_activities() {
1259          $modinfo = get_fast_modinfo($this->course);
1260          foreach ($modinfo->get_cms() as $cm) {
1261              if ($cm->completion != COMPLETION_TRACKING_NONE) {
1262                  return true;
1263              }
1264          }
1265          return false;
1266      }
1267  
1268      /**
1269       * Obtains a list of activities for which completion is enabled on the
1270       * course. The list is ordered by the section order of those activities.
1271       *
1272       * @return cm_info[] Array from $cmid => $cm of all activities with completion enabled,
1273       *   empty array if none
1274       */
1275      public function get_activities() {
1276          $modinfo = get_fast_modinfo($this->course);
1277          $result = array();
1278          foreach ($modinfo->get_cms() as $cm) {
1279              if ($cm->completion != COMPLETION_TRACKING_NONE && !$cm->deletioninprogress) {
1280                  $result[$cm->id] = $cm;
1281              }
1282          }
1283          return $result;
1284      }
1285  
1286      /**
1287       * Checks to see if the userid supplied has a tracked role in
1288       * this course
1289       *
1290       * @param int $userid User id
1291       * @return bool
1292       */
1293      public function is_tracked_user($userid) {
1294          return is_enrolled(context_course::instance($this->course->id), $userid, 'moodle/course:isincompletionreports', true);
1295      }
1296  
1297      /**
1298       * Returns the number of users whose progress is tracked in this course.
1299       *
1300       * Optionally supply a search's where clause, or a group id.
1301       *
1302       * @param string $where Where clause sql (use 'u.whatever' for user table fields)
1303       * @param array $whereparams Where clause params
1304       * @param int $groupid Group id
1305       * @return int Number of tracked users
1306       */
1307      public function get_num_tracked_users($where = '', $whereparams = array(), $groupid = 0) {
1308          global $DB;
1309  
1310          list($enrolledsql, $enrolledparams) = get_enrolled_sql(
1311                  context_course::instance($this->course->id), 'moodle/course:isincompletionreports', $groupid, true);
1312          $sql  = 'SELECT COUNT(eu.id) FROM (' . $enrolledsql . ') eu JOIN {user} u ON u.id = eu.id';
1313          if ($where) {
1314              $sql .= " WHERE $where";
1315          }
1316  
1317          $params = array_merge($enrolledparams, $whereparams);
1318          return $DB->count_records_sql($sql, $params);
1319      }
1320  
1321      /**
1322       * Return array of users whose progress is tracked in this course.
1323       *
1324       * Optionally supply a search's where clause, group id, sorting, paging.
1325       *
1326       * @param string $where Where clause sql, referring to 'u.' fields (optional)
1327       * @param array $whereparams Where clause params (optional)
1328       * @param int $groupid Group ID to restrict to (optional)
1329       * @param string $sort Order by clause (optional)
1330       * @param int $limitfrom Result start (optional)
1331       * @param int $limitnum Result max size (optional)
1332       * @param context $extracontext If set, includes extra user information fields
1333       *   as appropriate to display for current user in this context
1334       * @return array Array of user objects with standard user fields
1335       */
1336      public function get_tracked_users($where = '', $whereparams = array(), $groupid = 0,
1337               $sort = '', $limitfrom = '', $limitnum = '', context $extracontext = null) {
1338  
1339          global $DB;
1340  
1341          list($enrolledsql, $params) = get_enrolled_sql(
1342                  context_course::instance($this->course->id),
1343                  'moodle/course:isincompletionreports', $groupid, true);
1344  
1345          // TODO Does not support custom user profile fields (MDL-70456).
1346          $userfieldsapi = \core_user\fields::for_identity($extracontext, false)->with_name();
1347          $allusernames = $userfieldsapi->get_sql('u')->selects;
1348          $sql = 'SELECT u.id, u.idnumber ' . $allusernames;
1349          $sql .= ' FROM (' . $enrolledsql . ') eu JOIN {user} u ON u.id = eu.id';
1350  
1351          if ($where) {
1352              $sql .= " AND $where";
1353              $params = array_merge($params, $whereparams);
1354          }
1355  
1356          if ($sort) {
1357              $sql .= " ORDER BY $sort";
1358          }
1359  
1360          return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
1361      }
1362  
1363      /**
1364       * Obtains progress information across a course for all users on that course, or
1365       * for all users in a specific group. Intended for use when displaying progress.
1366       *
1367       * This includes only users who, in course context, have one of the roles for
1368       * which progress is tracked (the gradebookroles admin option) and are enrolled in course.
1369       *
1370       * Users are included (in the first array) even if they do not have
1371       * completion progress for any course-module.
1372       *
1373       * @param bool $sortfirstname If true, sort by first name, otherwise sort by
1374       *   last name
1375       * @param string $where Where clause sql (optional)
1376       * @param array $where_params Where clause params (optional)
1377       * @param int $groupid Group ID or 0 (default)/false for all groups
1378       * @param int $pagesize Number of users to actually return (optional)
1379       * @param int $start User to start at if paging (optional)
1380       * @param context $extracontext If set, includes extra user information fields
1381       *   as appropriate to display for current user in this context
1382       * @return stdClass with ->total and ->start (same as $start) and ->users;
1383       *   an array of user objects (like mdl_user id, firstname, lastname)
1384       *   containing an additional ->progress array of coursemoduleid => completionstate
1385       */
1386      public function get_progress_all($where = '', $where_params = array(), $groupid = 0,
1387              $sort = '', $pagesize = '', $start = '', context $extracontext = null) {
1388          global $CFG, $DB;
1389  
1390          // Get list of applicable users
1391          $users = $this->get_tracked_users($where, $where_params, $groupid, $sort,
1392                  $start, $pagesize, $extracontext);
1393  
1394          // Get progress information for these users in groups of 1, 000 (if needed)
1395          // to avoid making the SQL IN too long
1396          $results = array();
1397          $userids = array();
1398          foreach ($users as $user) {
1399              $userids[] = $user->id;
1400              $results[$user->id] = $user;
1401              $results[$user->id]->progress = array();
1402          }
1403  
1404          for($i=0; $i<count($userids); $i+=1000) {
1405              $blocksize = count($userids)-$i < 1000 ? count($userids)-$i : 1000;
1406  
1407              list($insql, $params) = $DB->get_in_or_equal(array_slice($userids, $i, $blocksize));
1408              array_splice($params, 0, 0, array($this->course->id));
1409              $rs = $DB->get_recordset_sql("
1410                  SELECT
1411                      cmc.*
1412                  FROM
1413                      {course_modules} cm
1414                      INNER JOIN {course_modules_completion} cmc ON cm.id=cmc.coursemoduleid
1415                  WHERE
1416                      cm.course=? AND cmc.userid $insql", $params);
1417              foreach ($rs as $progress) {
1418                  $progress = (object)$progress;
1419                  $results[$progress->userid]->progress[$progress->coursemoduleid] = $progress;
1420              }
1421              $rs->close();
1422          }
1423  
1424          return $results;
1425      }
1426  
1427      /**
1428       * Called by grade code to inform the completion system when a grade has
1429       * been changed. If the changed grade is used to determine completion for
1430       * the course-module, then the completion status will be updated.
1431       *
1432       * @param stdClass|cm_info $cm Course-module for item that owns grade
1433       * @param grade_item $item Grade item
1434       * @param stdClass $grade
1435       * @param bool $deleted
1436       */
1437      public function inform_grade_changed($cm, $item, $grade, $deleted) {
1438          // Bail out now if completion is not enabled for course-module, it is enabled
1439          // but is set to manual, grade is not used to compute completion, or this
1440          // is a different numbered grade
1441          if (!$this->is_enabled($cm) ||
1442              $cm->completion == COMPLETION_TRACKING_MANUAL ||
1443              is_null($cm->completiongradeitemnumber) ||
1444              $item->itemnumber != $cm->completiongradeitemnumber) {
1445              return;
1446          }
1447  
1448          // What is the expected result based on this grade?
1449          if ($deleted) {
1450              // Grade being deleted, so only change could be to make it incomplete
1451              $possibleresult = COMPLETION_INCOMPLETE;
1452          } else {
1453              $possibleresult = self::internal_get_grade_state($item, $grade);
1454          }
1455  
1456          // OK, let's update state based on this
1457          $this->update_state($cm, $possibleresult, $grade->userid);
1458      }
1459  
1460      /**
1461       * Calculates the completion state that would result from a graded item
1462       * (where grade-based completion is turned on) based on the actual grade
1463       * and settings.
1464       *
1465       * Internal function. Not private, so we can unit-test it.
1466       *
1467       * @param grade_item $item an instance of grade_item
1468       * @param grade_grade $grade an instance of grade_grade
1469       * @return int Completion state e.g. COMPLETION_INCOMPLETE
1470       */
1471      public static function internal_get_grade_state($item, $grade) {
1472          // If no grade is supplied or the grade doesn't have an actual value, then
1473          // this is not complete.
1474          if (!$grade || (is_null($grade->finalgrade) && is_null($grade->rawgrade))) {
1475              return COMPLETION_INCOMPLETE;
1476          }
1477  
1478          // Conditions to show pass/fail:
1479          // a) Grade has pass mark (default is 0.00000 which is boolean true so be careful)
1480          // b) Grade is visible (neither hidden nor hidden-until)
1481          if ($item->gradepass && $item->gradepass > 0.000009 && !$item->hidden) {
1482              // Use final grade if set otherwise raw grade
1483              $score = !is_null($grade->finalgrade) ? $grade->finalgrade : $grade->rawgrade;
1484  
1485              // We are displaying and tracking pass/fail
1486              if ($score >= $item->gradepass) {
1487                  return COMPLETION_COMPLETE_PASS;
1488              } else {
1489                  return COMPLETION_COMPLETE_FAIL;
1490              }
1491          } else {
1492              // Not displaying pass/fail, so just if there is a grade
1493              if (!is_null($grade->finalgrade) || !is_null($grade->rawgrade)) {
1494                  // Grade exists, so maybe complete now
1495                  return COMPLETION_COMPLETE;
1496              } else {
1497                  // Grade does not exist, so maybe incomplete now
1498                  return COMPLETION_INCOMPLETE;
1499              }
1500          }
1501      }
1502  
1503      /**
1504       * Aggregate activity completion state
1505       *
1506       * @param   int     $type   Aggregation type (COMPLETION_* constant)
1507       * @param   bool    $old    Old state
1508       * @param   bool    $new    New state
1509       * @return  bool
1510       */
1511      public static function aggregate_completion_states($type, $old, $new) {
1512          if ($type == COMPLETION_AND) {
1513              return $old && $new;
1514          } else {
1515              return $old || $new;
1516          }
1517      }
1518  
1519      /**
1520       * This is to be used only for system errors (things that shouldn't happen)
1521       * and not user-level errors.
1522       *
1523       * @global type $CFG
1524       * @param string $error Error string (will not be displayed to user unless debugging is enabled)
1525       * @throws moodle_exception Exception with the error string as debug info
1526       */
1527      public function internal_systemerror($error) {
1528          global $CFG;
1529          throw new moodle_exception('err_system','completion',
1530              $CFG->wwwroot.'/course/view.php?id='.$this->course->id,null,$error);
1531      }
1532  }
1533  
1534  /**
1535   * Aggregate criteria status's as per configured aggregation method.
1536   *
1537   * @param int $method COMPLETION_AGGREGATION_* constant.
1538   * @param bool $data Criteria completion status.
1539   * @param bool|null $state Aggregation state.
1540   */
1541  function completion_cron_aggregate($method, $data, &$state) {
1542      if ($method == COMPLETION_AGGREGATION_ALL) {
1543          if ($data && $state !== false) {
1544              $state = true;
1545          } else {
1546              $state = false;
1547          }
1548      } else if ($method == COMPLETION_AGGREGATION_ANY) {
1549          if ($data) {
1550              $state = true;
1551          } else if (!$data && $state === null) {
1552              $state = false;
1553          }
1554      }
1555  }