Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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

   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  defined('MOODLE_INTERNAL') || die();
  18  
  19  global $CFG;
  20  require_once($CFG->libdir.'/completionlib.php');
  21  
  22  /**
  23   * Completion tests.
  24   *
  25   * @package    core_completion
  26   * @category   test
  27   * @copyright  2008 Sam Marshall
  28   * @copyright  2013 Frédéric Massart
  29   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   * @coversDefaultClass \completion_info
  31   */
  32  class completionlib_test extends advanced_testcase {
  33      protected $course;
  34      protected $user;
  35      protected $module1;
  36      protected $module2;
  37  
  38      protected function mock_setup() {
  39          global $DB, $CFG, $USER;
  40  
  41          $this->resetAfterTest();
  42  
  43          $DB = $this->createMock(get_class($DB));
  44          $CFG->enablecompletion = COMPLETION_ENABLED;
  45          $USER = (object)array('id' => 314159);
  46      }
  47  
  48      /**
  49       * Create course with user and activities.
  50       */
  51      protected function setup_data() {
  52          global $DB, $CFG;
  53  
  54          $this->resetAfterTest();
  55  
  56          // Enable completion before creating modules, otherwise the completion data is not written in DB.
  57          $CFG->enablecompletion = true;
  58  
  59          // Create a course with activities.
  60          $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
  61          $this->user = $this->getDataGenerator()->create_user();
  62          $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id);
  63  
  64          $this->module1 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
  65          $this->module2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
  66      }
  67  
  68      /**
  69       * Asserts that two variables are equal.
  70       *
  71       * @param  mixed   $expected
  72       * @param  mixed   $actual
  73       * @param  string  $message
  74       * @param  float   $delta
  75       * @param  integer $maxDepth
  76       * @param  boolean $canonicalize
  77       * @param  boolean $ignoreCase
  78       */
  79      public static function assertEquals($expected, $actual, string $message = '', float $delta = 0, int $maxDepth = 10,
  80                                          bool $canonicalize = false, bool $ignoreCase = false): void {
  81          // Nasty cheating hack: prevent random failures on timemodified field.
  82          if (is_array($actual) && (is_object($expected) || is_array($expected))) {
  83              $actual = (object) $actual;
  84              $expected = (object) $expected;
  85          }
  86          if (is_object($expected) and is_object($actual)) {
  87              if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
  88                  if ($expected->timemodified + 1 == $actual->timemodified) {
  89                      $expected = clone($expected);
  90                      $expected->timemodified = $actual->timemodified;
  91                  }
  92              }
  93          }
  94          parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
  95      }
  96  
  97      /**
  98       * @covers ::is_enabled_for_site
  99       * @covers ::is_enabled
 100       */
 101      public function test_is_enabled() {
 102          global $CFG;
 103          $this->mock_setup();
 104  
 105          // Config alone.
 106          $CFG->enablecompletion = COMPLETION_DISABLED;
 107          $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
 108          $CFG->enablecompletion = COMPLETION_ENABLED;
 109          $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
 110  
 111          // Course.
 112          $course = (object)array('id' => 13);
 113          $c = new completion_info($course);
 114          $course->enablecompletion = COMPLETION_DISABLED;
 115          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
 116          $course->enablecompletion = COMPLETION_ENABLED;
 117          $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled());
 118          $CFG->enablecompletion = COMPLETION_DISABLED;
 119          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
 120  
 121          // Course and CM.
 122          $cm = new stdClass();
 123          $cm->completion = COMPLETION_TRACKING_MANUAL;
 124          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
 125          $CFG->enablecompletion = COMPLETION_ENABLED;
 126          $course->enablecompletion = COMPLETION_DISABLED;
 127          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
 128          $course->enablecompletion = COMPLETION_ENABLED;
 129          $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
 130          $cm->completion = COMPLETION_TRACKING_NONE;
 131          $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
 132          $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
 133          $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
 134      }
 135  
 136      /**
 137       * @covers ::update_state
 138       */
 139      public function test_update_state() {
 140          $this->mock_setup();
 141  
 142          $mockbuilder = $this->getMockBuilder('completion_info');
 143          $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data',
 144                                         'user_can_override_completion'));
 145          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 146          $cm = (object)array('id' => 13, 'course' => 42);
 147  
 148          // Not enabled, should do nothing.
 149          $c = $mockbuilder->getMock();
 150          $c->expects($this->once())
 151              ->method('is_enabled')
 152              ->with($cm)
 153              ->will($this->returnValue(false));
 154          $c->update_state($cm);
 155  
 156          // Enabled, but current state is same as possible result, do nothing.
 157          $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
 158          $c = $mockbuilder->getMock();
 159          $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
 160          $c->expects($this->once())
 161              ->method('is_enabled')
 162              ->with($cm)
 163              ->will($this->returnValue(true));
 164          $c->expects($this->once())
 165              ->method('get_data')
 166              ->will($this->returnValue($current));
 167          $c->update_state($cm, COMPLETION_COMPLETE);
 168  
 169          // Enabled, but current state is a specific one and new state is just
 170          // complete, so do nothing.
 171          $c = $mockbuilder->getMock();
 172          $current->completionstate = COMPLETION_COMPLETE_PASS;
 173          $c->expects($this->once())
 174              ->method('is_enabled')
 175              ->with($cm)
 176              ->will($this->returnValue(true));
 177          $c->expects($this->once())
 178              ->method('get_data')
 179              ->will($this->returnValue($current));
 180          $c->update_state($cm, COMPLETION_COMPLETE);
 181  
 182          // Manual, change state (no change).
 183          $c = $mockbuilder->getMock();
 184          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
 185          $current->completionstate = COMPLETION_COMPLETE;
 186          $c->expects($this->once())
 187              ->method('is_enabled')
 188              ->with($cm)
 189              ->will($this->returnValue(true));
 190          $c->expects($this->once())
 191              ->method('get_data')
 192              ->will($this->returnValue($current));
 193          $c->update_state($cm, COMPLETION_COMPLETE);
 194  
 195          // Manual, change state (change).
 196          $c = $mockbuilder->getMock();
 197          $c->expects($this->once())
 198              ->method('is_enabled')
 199              ->with($cm)
 200              ->will($this->returnValue(true));
 201          $c->expects($this->once())
 202              ->method('get_data')
 203              ->will($this->returnValue($current));
 204          $changed = clone($current);
 205          $changed->timemodified = time();
 206          $changed->completionstate = COMPLETION_INCOMPLETE;
 207          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 208          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 209          $c->expects($this->once())
 210              ->method('internal_set_data')
 211              ->with($cm, $comparewith);
 212          $c->update_state($cm, COMPLETION_INCOMPLETE);
 213  
 214          // Auto, change state.
 215          $c = $mockbuilder->getMock();
 216          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 217          $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
 218          $c->expects($this->once())
 219              ->method('is_enabled')
 220              ->with($cm)
 221              ->will($this->returnValue(true));
 222          $c->expects($this->once())
 223              ->method('get_data')
 224              ->will($this->returnValue($current));
 225          $c->expects($this->once())
 226              ->method('internal_get_state')
 227              ->will($this->returnValue(COMPLETION_COMPLETE_PASS));
 228          $changed = clone($current);
 229          $changed->timemodified = time();
 230          $changed->completionstate = COMPLETION_COMPLETE_PASS;
 231          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 232          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 233          $c->expects($this->once())
 234              ->method('internal_set_data')
 235              ->with($cm, $comparewith);
 236          $c->update_state($cm, COMPLETION_COMPLETE_PASS);
 237  
 238          // Manual tracking, change state by overriding it manually.
 239          $c = $mockbuilder->getMock();
 240          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
 241          $current1 = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
 242          $current2 = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
 243          $c->expects($this->exactly(2))
 244              ->method('is_enabled')
 245              ->with($cm)
 246              ->will($this->returnValue(true));
 247          $c->expects($this->exactly(1)) // Pretend the user has the required capability for overriding completion statuses.
 248              ->method('user_can_override_completion')
 249              ->will($this->returnValue(true));
 250          $c->expects($this->exactly(2))
 251              ->method('get_data')
 252              ->with($cm, false, 100)
 253              ->willReturnOnConsecutiveCalls($current1, $current2);
 254          $changed1 = clone($current1);
 255          $changed1->timemodified = time();
 256          $changed1->completionstate = COMPLETION_COMPLETE;
 257          $changed1->overrideby = 314159;
 258          $comparewith1 = new phpunit_constraint_object_is_equal_with_exceptions($changed1);
 259          $comparewith1->add_exception('timemodified', 'assertGreaterThanOrEqual');
 260          $changed2 = clone($current2);
 261          $changed2->timemodified = time();
 262          $changed2->overrideby = null;
 263          $changed2->completionstate = COMPLETION_INCOMPLETE;
 264          $comparewith2 = new phpunit_constraint_object_is_equal_with_exceptions($changed2);
 265          $comparewith2->add_exception('timemodified', 'assertGreaterThanOrEqual');
 266          $c->expects($this->exactly(2))
 267              ->method('internal_set_data')
 268              ->withConsecutive(
 269                  array($cm, $comparewith1),
 270                  array($cm, $comparewith2)
 271              );
 272          $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
 273          // And confirm that the status can be changed back to incomplete without an override.
 274          $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
 275  
 276          // Auto, change state via override, incomplete to complete.
 277          $c = $mockbuilder->getMock();
 278          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 279          $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
 280          $c->expects($this->once())
 281              ->method('is_enabled')
 282              ->with($cm)
 283              ->will($this->returnValue(true));
 284          $c->expects($this->once()) // Pretend the user has the required capability for overriding completion statuses.
 285              ->method('user_can_override_completion')
 286              ->will($this->returnValue(true));
 287          $c->expects($this->once())
 288              ->method('get_data')
 289              ->with($cm, false, 100)
 290              ->will($this->returnValue($current));
 291          $changed = clone($current);
 292          $changed->timemodified = time();
 293          $changed->completionstate = COMPLETION_COMPLETE;
 294          $changed->overrideby = 314159;
 295          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 296          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 297          $c->expects($this->once())
 298              ->method('internal_set_data')
 299              ->with($cm, $comparewith);
 300          $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
 301  
 302          // Now confirm the status can be changed back from complete to incomplete using an override.
 303          $c = $mockbuilder->getMock();
 304          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 305          $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2);
 306          $c->expects($this->once())
 307              ->method('is_enabled')
 308              ->with($cm)
 309              ->will($this->returnValue(true));
 310          $c->expects($this->Once()) // Pretend the user has the required capability for overriding completion statuses.
 311              ->method('user_can_override_completion')
 312              ->will($this->returnValue(true));
 313          $c->expects($this->once())
 314              ->method('get_data')
 315              ->with($cm, false, 100)
 316              ->will($this->returnValue($current));
 317          $changed = clone($current);
 318          $changed->timemodified = time();
 319          $changed->completionstate = COMPLETION_INCOMPLETE;
 320          $changed->overrideby = 314159;
 321          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 322          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 323          $c->expects($this->once())
 324              ->method('internal_set_data')
 325              ->with($cm, $comparewith);
 326          $c->update_state($cm, COMPLETION_INCOMPLETE, 100, true);
 327      }
 328  
 329      /**
 330       * Data provider for test_internal_get_state().
 331       *
 332       * @return array[]
 333       */
 334      public function internal_get_state_provider() {
 335          return [
 336              'View required, but not viewed yet' => [
 337                  COMPLETION_VIEW_REQUIRED, 1, '', COMPLETION_INCOMPLETE
 338              ],
 339              'View not required and not viewed yet' => [
 340                  COMPLETION_VIEW_NOT_REQUIRED, 1, '', COMPLETION_INCOMPLETE
 341              ],
 342              'View not required, grade required but no grade yet, $cm->modname not set' => [
 343                  COMPLETION_VIEW_NOT_REQUIRED, 1, 'modname', COMPLETION_INCOMPLETE
 344              ],
 345              'View not required, grade required but no grade yet, $cm->course not set' => [
 346                  COMPLETION_VIEW_NOT_REQUIRED, 1, 'course', COMPLETION_INCOMPLETE
 347              ],
 348              'View not required, grade not required' => [
 349                  COMPLETION_VIEW_NOT_REQUIRED, 0, '', COMPLETION_COMPLETE
 350              ],
 351          ];
 352      }
 353  
 354      /**
 355       * Test for completion_info::get_state().
 356       *
 357       * @dataProvider internal_get_state_provider
 358       * @param int $completionview
 359       * @param int $completionusegrade
 360       * @param string $unsetfield
 361       * @param int $expectedstate
 362       * @covers ::internal_get_state
 363       */
 364      public function test_internal_get_state(int $completionview, int $completionusegrade, string $unsetfield, int $expectedstate) {
 365          $this->setup_data();
 366  
 367          /** @var \mod_assign_generator $assigngenerator */
 368          $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
 369          $assign = $assigngenerator->create_instance([
 370              'course' => $this->course->id,
 371              'completion' => COMPLETION_ENABLED,
 372              'completionview' => $completionview,
 373              'completionusegrade' => $completionusegrade,
 374          ]);
 375  
 376          $userid = $this->user->id;
 377          $this->setUser($userid);
 378  
 379          $cm = get_coursemodule_from_instance('assign', $assign->id);
 380          if ($unsetfield) {
 381              unset($cm->$unsetfield);
 382          }
 383          // If view is required, but they haven't viewed it yet.
 384          $current = (object)['viewed' => COMPLETION_NOT_VIEWED];
 385  
 386          $completioninfo = new completion_info($this->course);
 387          $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, $current));
 388      }
 389  
 390      /**
 391       * Provider for the test_internal_get_state_with_grade_criteria.
 392       *
 393       * @return array
 394       */
 395      public function internal_get_state_with_grade_criteria_provider() {
 396          return [
 397              "Passing grade enabled and achieve. State should be COMPLETION_COMPLETE_PASS" => [
 398                  [
 399                      'completionusegrade' => 1,
 400                      'completionpassgrade' => 1,
 401                      'gradepass' => 50,
 402                  ],
 403                  50,
 404                  COMPLETION_COMPLETE_PASS
 405              ],
 406              "Passing grade enabled and not achieve. State should be COMPLETION_COMPLETE_FAIL" => [
 407                  [
 408                      'completionusegrade' => 1,
 409                      'completionpassgrade' => 1,
 410                      'gradepass' => 50,
 411                  ],
 412                  40,
 413                  COMPLETION_COMPLETE_FAIL
 414              ],
 415              "Passing grade not enabled with passing grade set." => [
 416                  [
 417                      'completionusegrade' => 1,
 418                      'gradepass' => 50,
 419                  ],
 420                  50,
 421                  COMPLETION_COMPLETE_PASS
 422              ],
 423              "Passing grade not enabled with passing grade not set." => [
 424                  [
 425                      'completionusegrade' => 1,
 426                  ],
 427                  90,
 428                  COMPLETION_COMPLETE
 429              ],
 430              "Passing grade not enabled with passing grade not set. No submission made." => [
 431                  [
 432                      'completionusegrade' => 1,
 433                  ],
 434                  null,
 435                  COMPLETION_INCOMPLETE
 436              ],
 437          ];
 438      }
 439  
 440      /**
 441       * Tests that the right completion state is being set based on the grade criteria.
 442       *
 443       * @dataProvider internal_get_state_with_grade_criteria_provider
 444       * @param array $completioncriteria The completion criteria to use
 445       * @param int|null $studentgrade Grade to assign to student
 446       * @param int $expectedstate Expected completion state
 447       * @covers ::internal_get_state
 448       */
 449      public function test_internal_get_state_with_grade_criteria(array $completioncriteria, ?int $studentgrade, int $expectedstate) {
 450          $this->setup_data();
 451  
 452          /** @var \mod_assign_generator $assigngenerator */
 453          $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
 454          $assign = $assigngenerator->create_instance([
 455              'course' => $this->course->id,
 456              'completion' => COMPLETION_ENABLED,
 457          ] + $completioncriteria);
 458  
 459          $userid = $this->user->id;
 460  
 461          $cm = get_coursemodule_from_instance('assign', $assign->id);
 462          $usercm = cm_info::create($cm, $userid);
 463  
 464          // Create a teacher account.
 465          $teacher = $this->getDataGenerator()->create_user();
 466          $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
 467          // Log in as the teacher.
 468          $this->setUser($teacher);
 469  
 470          // Grade the student for this assignment.
 471          $assign = new assign($usercm->context, $cm, $cm->course);
 472          if ($studentgrade) {
 473              $data = (object)[
 474                  'sendstudentnotifications' => false,
 475                  'attemptnumber' => 1,
 476                  'grade' => $studentgrade,
 477              ];
 478              $assign->save_grade($userid, $data);
 479          }
 480  
 481          // The target user already received a grade, so internal_get_state should be already complete.
 482          $completioninfo = new completion_info($this->course);
 483          $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, null));
 484      }
 485  
 486      /**
 487       * Covers the case where internal_get_state() is being called for a user different from the logged in user.
 488       *
 489       * @covers ::internal_get_state
 490       */
 491      public function test_internal_get_state_with_different_user() {
 492          $this->setup_data();
 493  
 494          /** @var \mod_assign_generator $assigngenerator */
 495          $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
 496          $assign = $assigngenerator->create_instance([
 497              'course' => $this->course->id,
 498              'completion' => COMPLETION_ENABLED,
 499              'completionusegrade' => 1,
 500          ]);
 501  
 502          $userid = $this->user->id;
 503  
 504          $cm = get_coursemodule_from_instance('assign', $assign->id);
 505          $usercm = cm_info::create($cm, $userid);
 506  
 507          // Create a teacher account.
 508          $teacher = $this->getDataGenerator()->create_user();
 509          $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
 510          // Log in as the teacher.
 511          $this->setUser($teacher);
 512  
 513          // Grade the student for this assignment.
 514          $assign = new assign($usercm->context, $cm, $cm->course);
 515          $data = (object)[
 516              'sendstudentnotifications' => false,
 517              'attemptnumber' => 1,
 518              'grade' => 90,
 519          ];
 520          $assign->save_grade($userid, $data);
 521  
 522          // The target user already received a grade, so internal_get_state should be already complete.
 523          $completioninfo = new completion_info($this->course);
 524          $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->internal_get_state($cm, $userid, null));
 525  
 526          // As the teacher which does not have a grade in this cm, internal_get_state should return incomplete.
 527          $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->internal_get_state($cm, $teacher->id, null));
 528      }
 529  
 530      /**
 531       * Test for internal_get_state() for an activity that supports custom completion.
 532       *
 533       * @covers ::internal_get_state
 534       */
 535      public function test_internal_get_state_with_custom_completion() {
 536          $this->setup_data();
 537  
 538          $choicerecord = [
 539              'course' => $this->course,
 540              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 541              'completionsubmit' => COMPLETION_ENABLED,
 542          ];
 543          $choice = $this->getDataGenerator()->create_module('choice', $choicerecord);
 544          $cminfo = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
 545  
 546          $completioninfo = new completion_info($this->course);
 547  
 548          // Fetch completion for the user who hasn't made a choice yet.
 549          $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
 550          $this->assertEquals(COMPLETION_INCOMPLETE, $completion);
 551  
 552          // Have the user make a choice.
 553          $choicewithoptions = choice_get_choice($choice->id);
 554          $optionids = array_keys($choicewithoptions->option);
 555          choice_user_submit_response($optionids[0], $choice, $this->user->id, $this->course, $cminfo);
 556          $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
 557          $this->assertEquals(COMPLETION_COMPLETE, $completion);
 558      }
 559  
 560      /**
 561       * @covers ::set_module_viewed
 562       */
 563      public function test_set_module_viewed() {
 564          $this->mock_setup();
 565  
 566          $mockbuilder = $this->getMockBuilder('completion_info');
 567          $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
 568          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 569          $cm = (object)array('id' => 13, 'course' => 42);
 570  
 571          // Not tracking completion, should do nothing.
 572          $c = $mockbuilder->getMock();
 573          $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
 574          $c->set_module_viewed($cm);
 575  
 576          // Tracking completion but completion is disabled, should do nothing.
 577          $c = $mockbuilder->getMock();
 578          $cm->completionview = COMPLETION_VIEW_REQUIRED;
 579          $c->expects($this->once())
 580              ->method('is_enabled')
 581              ->with($cm)
 582              ->will($this->returnValue(false));
 583          $c->set_module_viewed($cm);
 584  
 585          // Now it's enabled, we expect it to get data. If data already has
 586          // viewed, still do nothing.
 587          $c = $mockbuilder->getMock();
 588          $c->expects($this->once())
 589              ->method('is_enabled')
 590              ->with($cm)
 591              ->will($this->returnValue(true));
 592          $c->expects($this->once())
 593              ->method('get_data')
 594              ->with($cm, 0)
 595              ->will($this->returnValue((object)array('viewed' => COMPLETION_VIEWED)));
 596          $c->set_module_viewed($cm);
 597  
 598          // OK finally one that hasn't been viewed, now it should set it viewed
 599          // and update state.
 600          $c = $mockbuilder->getMock();
 601          $c->expects($this->once())
 602              ->method('is_enabled')
 603              ->with($cm)
 604              ->will($this->returnValue(true));
 605          $c->expects($this->once())
 606              ->method('get_data')
 607              ->with($cm, false, 1337)
 608              ->will($this->returnValue((object)array('viewed' => COMPLETION_NOT_VIEWED)));
 609          $c->expects($this->once())
 610              ->method('internal_set_data')
 611              ->with($cm, (object)array('viewed' => COMPLETION_VIEWED));
 612          $c->expects($this->once())
 613              ->method('update_state')
 614              ->with($cm, COMPLETION_COMPLETE, 1337);
 615          $c->set_module_viewed($cm, 1337);
 616      }
 617  
 618      /**
 619       * @covers ::count_user_data
 620       */
 621      public function test_count_user_data() {
 622          global $DB;
 623          $this->mock_setup();
 624  
 625          $course = (object)array('id' => 13);
 626          $cm = (object)array('id' => 42);
 627  
 628          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 629          $DB->expects($this->once())
 630              ->method('get_field_sql')
 631              ->will($this->returnValue(666));
 632  
 633          $c = new completion_info($course);
 634          $this->assertEquals(666, $c->count_user_data($cm));
 635      }
 636  
 637      /**
 638       * @covers ::delete_all_state
 639       */
 640      public function test_delete_all_state() {
 641          global $DB;
 642          $this->mock_setup();
 643  
 644          $course = (object)array('id' => 13);
 645          $cm = (object)array('id' => 42, 'course' => 13);
 646          $c = new completion_info($course);
 647  
 648          // Check it works ok without data in session.
 649          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 650          $DB->expects($this->once())
 651              ->method('delete_records')
 652              ->with('course_modules_completion', array('coursemoduleid' => 42))
 653              ->will($this->returnValue(true));
 654          $c->delete_all_state($cm);
 655      }
 656  
 657      /**
 658       * @covers ::reset_all_state
 659       */
 660      public function test_reset_all_state() {
 661          global $DB;
 662          $this->mock_setup();
 663  
 664          $mockbuilder = $this->getMockBuilder('completion_info');
 665          $mockbuilder->onlyMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
 666          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 667          $c = $mockbuilder->getMock();
 668  
 669          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 670  
 671          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 672          $DB->expects($this->once())
 673              ->method('get_recordset')
 674              ->will($this->returnValue(
 675                  new core_completionlib_fake_recordset(array((object)array('id' => 1, 'userid' => 100),
 676                      (object)array('id' => 2, 'userid' => 101)))));
 677  
 678          $c->expects($this->once())
 679              ->method('delete_all_state')
 680              ->with($cm);
 681  
 682          $c->expects($this->once())
 683              ->method('get_tracked_users')
 684              ->will($this->returnValue(array(
 685              (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
 686              (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
 687  
 688          $c->expects($this->exactly(3))
 689              ->method('update_state')
 690              ->withConsecutive(
 691                  array($cm, COMPLETION_UNKNOWN, 100),
 692                  array($cm, COMPLETION_UNKNOWN, 101),
 693                  array($cm, COMPLETION_UNKNOWN, 201)
 694              );
 695  
 696          $c->reset_all_state($cm);
 697      }
 698  
 699      /**
 700       * Data provider for test_get_data().
 701       *
 702       * @return array[]
 703       */
 704      public function get_data_provider() {
 705          return [
 706              'No completion record' => [
 707                  false, true, false, COMPLETION_INCOMPLETE
 708              ],
 709              'Not completed' => [
 710                  false, true, true, COMPLETION_INCOMPLETE
 711              ],
 712              'Completed' => [
 713                  false, true, true, COMPLETION_COMPLETE
 714              ],
 715              'Whole course, complete' => [
 716                  true, true, true, COMPLETION_COMPLETE
 717              ],
 718              'Get data for another user, result should be not cached' => [
 719                  false, false, true,  COMPLETION_INCOMPLETE
 720              ],
 721              'Get data for another user, including whole course, result should be not cached' => [
 722                  true, false, true,  COMPLETION_INCOMPLETE
 723              ],
 724          ];
 725      }
 726  
 727      /**
 728       * Tests for completion_info::get_data().
 729       *
 730       * @dataProvider get_data_provider
 731       * @param bool $wholecourse Whole course parameter for get_data().
 732       * @param bool $sameuser Whether the user calling get_data() is the user itself.
 733       * @param bool $hasrecord Whether to create a course_modules_completion record.
 734       * @param int $completion The completion state expected.
 735       * @covers ::get_data
 736       */
 737      public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion) {
 738          global $DB;
 739  
 740          $this->setup_data();
 741          $user = $this->user;
 742  
 743          $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
 744          $choice = $choicegenerator->create_instance([
 745              'course' => $this->course->id,
 746              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 747              'completionview' => true,
 748              'completionsubmit' => true,
 749          ]);
 750  
 751          $cm = get_coursemodule_from_instance('choice', $choice->id);
 752  
 753          // Let's manually create a course completion record instead of going through the hoops to complete an activity.
 754          if ($hasrecord) {
 755              $cmcompletionrecord = (object)[
 756                  'coursemoduleid' => $cm->id,
 757                  'userid' => $user->id,
 758                  'completionstate' => $completion,
 759                  'viewed' => 0,
 760                  'overrideby' => null,
 761                  'timemodified' => 0,
 762              ];
 763              $DB->insert_record('course_modules_completion', $cmcompletionrecord);
 764          }
 765  
 766          // Whether we expect for the returned completion data to be stored in the cache.
 767          $iscached = true;
 768  
 769          if (!$sameuser) {
 770              $iscached = false;
 771              $this->setAdminUser();
 772          } else {
 773              $this->setUser($user);
 774          }
 775  
 776          // Mock other completion data.
 777          $completioninfo = new completion_info($this->course);
 778  
 779          $result = $completioninfo->get_data($cm, $wholecourse, $user->id);
 780  
 781          // Course module ID of the returned completion data must match this activity's course module ID.
 782          $this->assertEquals($cm->id, $result->coursemoduleid);
 783          // User ID of the returned completion data must match the user's ID.
 784          $this->assertEquals($user->id, $result->userid);
 785          // The completion state of the returned completion data must match the expected completion state.
 786          $this->assertEquals($completion, $result->completionstate);
 787  
 788          // If the user has no completion record, then the default record should be returned.
 789          if (!$hasrecord) {
 790              $this->assertEquals(0, $result->id);
 791          }
 792  
 793          // Check that we are including relevant completion data for the module.
 794          if (!$wholecourse) {
 795              $this->assertTrue(property_exists($result, 'viewed'));
 796              $this->assertTrue(property_exists($result, 'customcompletion'));
 797          }
 798      }
 799  
 800      /**
 801       * @covers ::get_data
 802       */
 803      public function test_get_data_successive_calls(): void {
 804          global $DB;
 805  
 806          $this->setup_data();
 807          $this->setUser($this->user);
 808  
 809          $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
 810          $choice = $choicegenerator->create_instance([
 811              'course' => $this->course->id,
 812              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 813              'completionview' => true,
 814              'completionsubmit' => true,
 815          ]);
 816  
 817          $cm = get_coursemodule_from_instance('choice', $choice->id);
 818  
 819          // Let's manually create a course completion record instead of going through the hoops to complete an activity.
 820          $cmcompletionrecord = (object) [
 821              'coursemoduleid' => $cm->id,
 822              'userid' => $this->user->id,
 823              'completionstate' => COMPLETION_NOT_VIEWED,
 824              'viewed' => 0,
 825              'overrideby' => null,
 826              'timemodified' => 0,
 827          ];
 828          $DB->insert_record('course_modules_completion', $cmcompletionrecord);
 829  
 830          // Mock other completion data.
 831          $completioninfo = new completion_info($this->course);
 832  
 833          $modinfo = get_fast_modinfo($this->course);
 834          $results = [];
 835          foreach ($modinfo->cms as $testcm) {
 836              $result = $completioninfo->get_data($testcm, true);
 837              $this->assertTrue(property_exists($result, 'id'));
 838              $this->assertTrue(property_exists($result, 'coursemoduleid'));
 839              $this->assertTrue(property_exists($result, 'userid'));
 840              $this->assertTrue(property_exists($result, 'completionstate'));
 841              $this->assertTrue(property_exists($result, 'viewed'));
 842              $this->assertTrue(property_exists($result, 'overrideby'));
 843              $this->assertTrue(property_exists($result, 'timemodified'));
 844              $this->assertFalse(property_exists($result, 'other_cm_completion_data_fetched'));
 845  
 846              $this->assertEquals($testcm->id, $result->coursemoduleid);
 847              $this->assertEquals($this->user->id, $result->userid);
 848              $this->assertEquals(0, $result->viewed);
 849  
 850              $results[$testcm->id] = $result;
 851          }
 852  
 853          $result = $completioninfo->get_data($cm);
 854          $this->assertTrue(property_exists($result, 'customcompletion'));
 855  
 856          // The data should match when fetching modules individually.
 857          (cache::make('core', 'completion'))->purge();
 858          foreach ($modinfo->cms as $testcm) {
 859              $result = $completioninfo->get_data($testcm, false);
 860              $this->assertEquals($result, $results[$testcm->id]);
 861          }
 862      }
 863  
 864      /**
 865       * Tests for completion_info::get_other_cm_completion_data().
 866       *
 867       * @covers ::get_other_cm_completion_data
 868       */
 869      public function test_get_other_cm_completion_data() {
 870          global $DB;
 871  
 872          $this->setup_data();
 873          $user = $this->user;
 874  
 875          $this->setAdminUser();
 876  
 877          $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
 878          $choice = $choicegenerator->create_instance([
 879              'course' => $this->course->id,
 880              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 881              'completionsubmit' => true,
 882          ]);
 883  
 884          $cmchoice = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
 885  
 886          $choice2 = $choicegenerator->create_instance([
 887              'course' => $this->course->id,
 888              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 889          ]);
 890  
 891          $cmchoice2 = cm_info::create(get_coursemodule_from_instance('choice', $choice2->id));
 892  
 893          $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
 894          $workshop = $workshopgenerator->create_instance([
 895              'course' => $this->course->id,
 896              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 897              // Submission grade required.
 898              'completiongradeitemnumber' => 0,
 899              'completionpassgrade' => 1,
 900          ]);
 901  
 902          $cmworkshop = cm_info::create(get_coursemodule_from_instance('workshop', $workshop->id));
 903  
 904          $completioninfo = new completion_info($this->course);
 905  
 906          $method = new ReflectionMethod("completion_info", "get_other_cm_completion_data");
 907          $method->setAccessible(true);
 908  
 909          // Check that fetching data for a module with custom completion provides its info.
 910          $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
 911  
 912          $this->assertArrayHasKey('customcompletion', $choicecompletiondata);
 913          $this->assertArrayHasKey('completionsubmit', $choicecompletiondata['customcompletion']);
 914          $this->assertEquals(COMPLETION_INCOMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
 915  
 916          // Mock a choice answer so user has completed the requirement.
 917          $choicemockinfo = [
 918              'choiceid' => $cmchoice->instance,
 919              'userid' => $this->user->id
 920          ];
 921          $DB->insert_record('choice_answers', $choicemockinfo, false);
 922  
 923          // Confirm fetching again reflects the completion.
 924          $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
 925          $this->assertEquals(COMPLETION_COMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
 926  
 927          // Check that fetching data for a module with no custom completion still provides its grade completion status.
 928          $workshopcompletiondata = $method->invoke($completioninfo, $cmworkshop, $user->id);
 929  
 930          $this->assertArrayHasKey('completiongrade', $workshopcompletiondata);
 931          $this->assertArrayHasKey('passgrade', $workshopcompletiondata);
 932          $this->assertArrayNotHasKey('customcompletion', $workshopcompletiondata);
 933          $this->assertEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['completiongrade']);
 934          $this->assertEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['passgrade']);
 935  
 936          // Check that fetching data for a module with no completion conditions does not provide any data.
 937          $choice2completiondata = $method->invoke($completioninfo, $cmchoice2, $user->id);
 938          $this->assertEmpty($choice2completiondata);
 939      }
 940  
 941      /**
 942       * @covers ::internal_set_data
 943       */
 944      public function test_internal_set_data() {
 945          global $DB;
 946          $this->setup_data();
 947  
 948          $this->setUser($this->user);
 949          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
 950          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
 951          $cm = get_coursemodule_from_instance('forum', $forum->id);
 952          $c = new completion_info($this->course);
 953  
 954          // 1) Test with new data.
 955          $data = new stdClass();
 956          $data->id = 0;
 957          $data->userid = $this->user->id;
 958          $data->coursemoduleid = $cm->id;
 959          $data->completionstate = COMPLETION_COMPLETE;
 960          $data->timemodified = time();
 961          $data->viewed = COMPLETION_NOT_VIEWED;
 962          $data->overrideby = null;
 963  
 964          $c->internal_set_data($cm, $data);
 965          $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
 966          $this->assertEquals($d1, $data->id);
 967          $cache = cache::make('core', 'completion');
 968          // Cache was not set for another user.
 969          $cachevalue = $cache->get("{$data->userid}_{$cm->course}");
 970          $this->assertEquals([
 971              'cacherev' => $this->course->cacherev,
 972              $cm->id => array_merge(
 973                  (array) $data,
 974                  ['other_cm_completion_data_fetched' => true]
 975              ),
 976          ],
 977          $cachevalue);
 978  
 979          // 2) Test with existing data and for different user.
 980          $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
 981          $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
 982          $newuser = $this->getDataGenerator()->create_user();
 983  
 984          $d2 = new stdClass();
 985          $d2->id = 7;
 986          $d2->userid = $newuser->id;
 987          $d2->coursemoduleid = $cm2->id;
 988          $d2->completionstate = COMPLETION_COMPLETE;
 989          $d2->timemodified = time();
 990          $d2->viewed = COMPLETION_NOT_VIEWED;
 991          $d2->overrideby = null;
 992          $c->internal_set_data($cm2, $d2);
 993          // Cache for current user returns the data.
 994          $cachevalue = $cache->get($data->userid . '_' . $cm->course);
 995          $this->assertEquals(array_merge(
 996              (array) $data,
 997              ['other_cm_completion_data_fetched' => true]
 998          ), $cachevalue[$cm->id]);
 999  
1000          // Cache for another user is not filled.
1001          $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
1002  
1003          // 3) Test where it THINKS the data is new (from cache) but actually in the database it has been set since.
1004          $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1005          $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
1006          $newuser2 = $this->getDataGenerator()->create_user();
1007          $d3 = new stdClass();
1008          $d3->id = 13;
1009          $d3->userid = $newuser2->id;
1010          $d3->coursemoduleid = $cm3->id;
1011          $d3->completionstate = COMPLETION_COMPLETE;
1012          $d3->timemodified = time();
1013          $d3->viewed = COMPLETION_NOT_VIEWED;
1014          $d3->overrideby = null;
1015          $DB->insert_record('course_modules_completion', $d3);
1016          $c->internal_set_data($cm, $data);
1017  
1018          // 4) Test instant course completions.
1019          $dataactivity = $this->getDataGenerator()->create_module('data', array('course' => $this->course->id),
1020              array('completion' => 1));
1021          $cm = get_coursemodule_from_instance('data', $dataactivity->id);
1022          $c = new completion_info($this->course);
1023          $cmdata = get_coursemodule_from_id('data', $dataactivity->cmid);
1024  
1025          // Add activity completion criteria.
1026          $criteriadata = new stdClass();
1027          $criteriadata->id = $this->course->id;
1028          $criteriadata->criteria_activity = array();
1029          // Some activities.
1030          $criteriadata->criteria_activity[$cmdata->id] = 1;
1031          $class = 'completion_criteria_activity';
1032          $criterion = new $class();
1033          $criterion->update_config($criteriadata);
1034  
1035          $actual = $DB->get_records('course_completions');
1036          $this->assertEmpty($actual);
1037  
1038          $data->coursemoduleid = $cm->id;
1039          $c->internal_set_data($cm, $data);
1040          $actual = $DB->get_records('course_completions');
1041          $this->assertEquals(1, count($actual));
1042          $this->assertEquals($this->user->id, reset($actual)->userid);
1043  
1044          $data->userid = $newuser2->id;
1045          $c->internal_set_data($cm, $data, true);
1046          $actual = $DB->get_records('course_completions');
1047          $this->assertEquals(1, count($actual));
1048          $this->assertEquals($this->user->id, reset($actual)->userid);
1049      }
1050  
1051      /**
1052       * @covers ::get_progress_all
1053       */
1054      public function test_get_progress_all_few() {
1055          global $DB;
1056          $this->mock_setup();
1057  
1058          $mockbuilder = $this->getMockBuilder('completion_info');
1059          $mockbuilder->onlyMethods(array('get_tracked_users'));
1060          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1061          $c = $mockbuilder->getMock();
1062  
1063          // With few results.
1064          $c->expects($this->once())
1065              ->method('get_tracked_users')
1066              ->with(false,  array(),  0,  '',  '',  '',  null)
1067              ->will($this->returnValue(array(
1068                  (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
1069                  (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
1070          $DB->expects($this->once())
1071              ->method('get_in_or_equal')
1072              ->with(array(100, 201))
1073              ->will($this->returnValue(array(' IN (100, 201)', array())));
1074          $progress1 = (object)array('userid' => 100, 'coursemoduleid' => 13);
1075          $progress2 = (object)array('userid' => 201, 'coursemoduleid' => 14);
1076          $DB->expects($this->once())
1077              ->method('get_recordset_sql')
1078              ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
1079  
1080          $this->assertEquals(array(
1081                  100 => (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh',
1082                      'progress' => array(13 => $progress1)),
1083                  201 => (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy',
1084                      'progress' => array(14 => $progress2)),
1085              ), $c->get_progress_all(false));
1086      }
1087  
1088      /**
1089       * @covers ::get_progress_all
1090       */
1091      public function test_get_progress_all_lots() {
1092          global $DB;
1093          $this->mock_setup();
1094  
1095          $mockbuilder = $this->getMockBuilder('completion_info');
1096          $mockbuilder->onlyMethods(array('get_tracked_users'));
1097          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1098          $c = $mockbuilder->getMock();
1099  
1100          $tracked = array();
1101          $ids = array();
1102          $progress = array();
1103          // With more than 1000 results.
1104          for ($i = 100; $i < 2000; $i++) {
1105              $tracked[] = (object)array('id' => $i, 'firstname' => 'frog', 'lastname' => $i);
1106              $ids[] = $i;
1107              $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 13);
1108              $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 14);
1109          }
1110          $c->expects($this->once())
1111              ->method('get_tracked_users')
1112              ->with(true,  3,  0,  '',  '',  '',  null)
1113              ->will($this->returnValue($tracked));
1114          $DB->expects($this->exactly(2))
1115              ->method('get_in_or_equal')
1116              ->withConsecutive(
1117                  array(array_slice($ids, 0, 1000)),
1118                  array(array_slice($ids, 1000))
1119              )
1120              ->willReturnOnConsecutiveCalls(
1121                  array(' IN whatever', array()),
1122                  array(' IN whatever2', array()));
1123          $DB->expects($this->exactly(2))
1124              ->method('get_recordset_sql')
1125              ->willReturnOnConsecutiveCalls(
1126                  new core_completionlib_fake_recordset(array_slice($progress, 0, 1000)),
1127                  new core_completionlib_fake_recordset(array_slice($progress, 1000)));
1128  
1129          $result = $c->get_progress_all(true, 3);
1130          $resultok = true;
1131          $resultok = $resultok && ($ids == array_keys($result));
1132  
1133          foreach ($result as $userid => $data) {
1134              $resultok = $resultok && $data->firstname == 'frog';
1135              $resultok = $resultok && $data->lastname == $userid;
1136              $resultok = $resultok && $data->id == $userid;
1137              $cms = $data->progress;
1138              $resultok = $resultok && (array(13, 14) == array_keys($cms));
1139              $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 13) == $cms[13]);
1140              $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 14) == $cms[14]);
1141          }
1142          $this->assertTrue($resultok);
1143          $this->assertCount(count($tracked), $result);
1144      }
1145  
1146      /**
1147       * @covers ::inform_grade_changed
1148       */
1149      public function test_inform_grade_changed() {
1150          $this->mock_setup();
1151  
1152          $mockbuilder = $this->getMockBuilder('completion_info');
1153          $mockbuilder->onlyMethods(array('is_enabled', 'update_state'));
1154          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1155  
1156          $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => null);
1157          $item = (object)array('itemnumber' => 3,  'gradepass' => 1,  'hidden' => 0);
1158          $grade = (object)array('userid' => 31337,  'finalgrade' => 0,  'rawgrade' => 0);
1159  
1160          // Not enabled (should do nothing).
1161          $c = $mockbuilder->getMock();
1162          $c->expects($this->once())
1163              ->method('is_enabled')
1164              ->with($cm)
1165              ->will($this->returnValue(false));
1166          $c->inform_grade_changed($cm, $item, $grade, false);
1167  
1168          // Enabled but still no grade completion required,  should still do nothing.
1169          $c = $mockbuilder->getMock();
1170          $c->expects($this->once())
1171              ->method('is_enabled')
1172              ->with($cm)
1173              ->will($this->returnValue(true));
1174          $c->inform_grade_changed($cm, $item, $grade, false);
1175  
1176          // Enabled and completion required but item number is wrong,  does nothing.
1177          $c = $mockbuilder->getMock();
1178          $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 7);
1179          $c->expects($this->once())
1180              ->method('is_enabled')
1181              ->with($cm)
1182              ->will($this->returnValue(true));
1183          $c->inform_grade_changed($cm, $item, $grade, false);
1184  
1185          // Enabled and completion required and item number right. It is supposed
1186          // to call update_state with the new potential state being obtained from
1187          // internal_get_grade_state.
1188          $c = $mockbuilder->getMock();
1189          $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1190          $grade = (object)array('userid' => 31337,  'finalgrade' => 1,  'rawgrade' => 0);
1191          $c->expects($this->once())
1192              ->method('is_enabled')
1193              ->with($cm)
1194              ->will($this->returnValue(true));
1195          $c->expects($this->once())
1196              ->method('update_state')
1197              ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
1198              ->will($this->returnValue(true));
1199          $c->inform_grade_changed($cm, $item, $grade, false);
1200  
1201          // Same as above but marked deleted. It is supposed to call update_state
1202          // with new potential state being COMPLETION_INCOMPLETE.
1203          $c = $mockbuilder->getMock();
1204          $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1205          $grade = (object)array('userid' => 31337,  'finalgrade' => 1,  'rawgrade' => 0);
1206          $c->expects($this->once())
1207              ->method('is_enabled')
1208              ->with($cm)
1209              ->will($this->returnValue(true));
1210          $c->expects($this->once())
1211              ->method('update_state')
1212              ->with($cm, COMPLETION_INCOMPLETE, 31337)
1213              ->will($this->returnValue(true));
1214          $c->inform_grade_changed($cm, $item, $grade, true);
1215      }
1216  
1217      /**
1218       * @covers ::internal_get_grade_state
1219       */
1220      public function test_internal_get_grade_state() {
1221          $this->mock_setup();
1222  
1223          $item = new stdClass;
1224          $grade = new stdClass;
1225  
1226          $item->gradepass = 4;
1227          $item->hidden = 0;
1228          $grade->rawgrade = 4.0;
1229          $grade->finalgrade = null;
1230  
1231          // Grade has pass mark and is not hidden,  user passes.
1232          $this->assertEquals(
1233              COMPLETION_COMPLETE_PASS,
1234              completion_info::internal_get_grade_state($item, $grade));
1235  
1236          // Same but user fails.
1237          $grade->rawgrade = 3.9;
1238          $this->assertEquals(
1239              COMPLETION_COMPLETE_FAIL,
1240              completion_info::internal_get_grade_state($item, $grade));
1241  
1242          // User fails on raw grade but passes on final.
1243          $grade->finalgrade = 4.0;
1244          $this->assertEquals(
1245              COMPLETION_COMPLETE_PASS,
1246              completion_info::internal_get_grade_state($item, $grade));
1247  
1248          // Item is hidden.
1249          $item->hidden = 1;
1250          $this->assertEquals(
1251              COMPLETION_COMPLETE,
1252              completion_info::internal_get_grade_state($item, $grade));
1253  
1254          // Item isn't hidden but has no pass mark.
1255          $item->hidden = 0;
1256          $item->gradepass = 0;
1257          $this->assertEquals(
1258              COMPLETION_COMPLETE,
1259              completion_info::internal_get_grade_state($item, $grade));
1260  
1261          // Item is hidden, but returnpassfail is true and the grade is passing.
1262          $item->hidden = 1;
1263          $item->gradepass = 4;
1264          $grade->finalgrade = 5.0;
1265          $this->assertEquals(
1266              COMPLETION_COMPLETE_PASS,
1267              completion_info::internal_get_grade_state($item, $grade, true));
1268  
1269          // Item is hidden, but returnpassfail is true and the grade is failing.
1270          $item->hidden = 1;
1271          $item->gradepass = 4;
1272          $grade->finalgrade = 3.0;
1273          $this->assertEquals(
1274              COMPLETION_COMPLETE_FAIL_HIDDEN,
1275              completion_info::internal_get_grade_state($item, $grade, true));
1276  
1277          // Item is not hidden, but returnpassfail is true and the grade is failing.
1278          $item->hidden = 0;
1279          $item->gradepass = 4;
1280          $grade->finalgrade = 3.0;
1281          $this->assertEquals(
1282              COMPLETION_COMPLETE_FAIL,
1283              completion_info::internal_get_grade_state($item, $grade, true));
1284      }
1285  
1286      /**
1287       * @test ::get_activities
1288       */
1289      public function test_get_activities() {
1290          global $CFG;
1291          $this->resetAfterTest();
1292  
1293          // Enable completion before creating modules, otherwise the completion data is not written in DB.
1294          $CFG->enablecompletion = true;
1295  
1296          // Create a course with mixed auto completion data.
1297          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1298          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1299          $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
1300          $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1301          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1302          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
1303          $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
1304  
1305          $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
1306          $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
1307          $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
1308  
1309          // Create data in another course to make sure it's not considered.
1310          $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1311          $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
1312          $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
1313          $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
1314  
1315          $c = new completion_info($course);
1316          $activities = $c->get_activities();
1317          $this->assertCount(3, $activities);
1318          $this->assertTrue(isset($activities[$forum->cmid]));
1319          $this->assertSame($forum->name, $activities[$forum->cmid]->name);
1320          $this->assertTrue(isset($activities[$page->cmid]));
1321          $this->assertSame($page->name, $activities[$page->cmid]->name);
1322          $this->assertTrue(isset($activities[$data->cmid]));
1323          $this->assertSame($data->name, $activities[$data->cmid]->name);
1324  
1325          $this->assertFalse(isset($activities[$forum2->cmid]));
1326          $this->assertFalse(isset($activities[$page2->cmid]));
1327          $this->assertFalse(isset($activities[$data2->cmid]));
1328      }
1329  
1330      /**
1331       * @test ::has_activities
1332       */
1333      public function test_has_activities() {
1334          global $CFG;
1335          $this->resetAfterTest();
1336  
1337          // Enable completion before creating modules, otherwise the completion data is not written in DB.
1338          $CFG->enablecompletion = true;
1339  
1340          // Create a course with mixed auto completion data.
1341          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1342          $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1343          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1344          $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1345          $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1346          $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
1347  
1348          $c1 = new completion_info($course);
1349          $c2 = new completion_info($course2);
1350  
1351          $this->assertTrue($c1->has_activities());
1352          $this->assertFalse($c2->has_activities());
1353      }
1354  
1355      /**
1356       * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
1357       *
1358       * @covers ::delete_course_completion_data
1359       * @covers ::delete_all_completion_data
1360       */
1361      public function test_course_delete_prerequisite() {
1362          global $DB;
1363  
1364          $this->setup_data();
1365  
1366          $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
1367  
1368          $criteriadata = (object) [
1369              'id' => $this->course->id,
1370              'criteria_course' => [$courseprerequisite->id],
1371          ];
1372  
1373          /** @var completion_criteria_course $criteria */
1374          $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]);
1375          $criteria->update_config($criteriadata);
1376  
1377          // Sanity test.
1378          $this->assertTrue($DB->record_exists('course_completion_criteria', [
1379              'course' => $this->course->id,
1380              'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1381              'courseinstance' => $courseprerequisite->id,
1382          ]));
1383  
1384          // Deleting the prerequisite course should remove the completion criteria.
1385          delete_course($courseprerequisite, false);
1386  
1387          $this->assertFalse($DB->record_exists('course_completion_criteria', [
1388              'course' => $this->course->id,
1389              'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1390              'courseinstance' => $courseprerequisite->id,
1391          ]));
1392      }
1393  
1394      /**
1395       * Test course module completion update event.
1396       *
1397       * @covers \core\event\course_module_completion_updated
1398       */
1399      public function test_course_module_completion_updated_event() {
1400          global $USER, $CFG;
1401  
1402          $this->setup_data();
1403  
1404          $this->setAdminUser();
1405  
1406          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1407          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1408  
1409          $c = new completion_info($this->course);
1410          $activities = $c->get_activities();
1411          $this->assertEquals(1, count($activities));
1412          $this->assertTrue(isset($activities[$forum->cmid]));
1413          $this->assertEquals($activities[$forum->cmid]->name, $forum->name);
1414  
1415          $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
1416          $current->completionstate = COMPLETION_COMPLETE;
1417          $current->timemodified = time();
1418          $sink = $this->redirectEvents();
1419          $c->internal_set_data($activities[$forum->cmid], $current);
1420          $events = $sink->get_events();
1421          $event = reset($events);
1422          $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
1423          $this->assertEquals($forum->cmid,
1424              $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
1425          $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
1426          $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
1427          $this->assertEquals($USER->id, $event->userid);
1428          $this->assertEquals($this->user->id, $event->relateduserid);
1429          $this->assertInstanceOf('moodle_url', $event->get_url());
1430          $this->assertEventLegacyData($current, $event);
1431      }
1432  
1433      /**
1434       * Test course completed event.
1435       *
1436       * @covers \core\event\course_completed
1437       */
1438      public function test_course_completed_event() {
1439          global $USER;
1440  
1441          $this->setup_data();
1442          $this->setAdminUser();
1443  
1444          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1445          $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1446  
1447          // Mark course as complete and get triggered event.
1448          $sink = $this->redirectEvents();
1449          $ccompletion->mark_complete();
1450          $events = $sink->get_events();
1451          $event = reset($events);
1452  
1453          $this->assertInstanceOf('\core\event\course_completed', $event);
1454          $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
1455          $this->assertEquals($this->course->id, $event->courseid);
1456          $this->assertEquals($USER->id, $event->userid);
1457          $this->assertEquals($this->user->id, $event->relateduserid);
1458          $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
1459          $this->assertInstanceOf('moodle_url', $event->get_url());
1460          $data = $ccompletion->get_record_data();
1461          $this->assertEventLegacyData($data, $event);
1462      }
1463  
1464      /**
1465       * Test course completed message.
1466       *
1467       * @covers \core\event\course_completed
1468       */
1469      public function test_course_completed_message() {
1470          $this->setup_data();
1471          $this->setAdminUser();
1472  
1473          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1474          $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1475  
1476          // Mark course as complete and get the message.
1477          $sink = $this->redirectMessages();
1478          $ccompletion->mark_complete();
1479          $messages = $sink->get_messages();
1480          $sink->close();
1481  
1482          $this->assertCount(1, $messages);
1483          $message = array_pop($messages);
1484  
1485          $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom);
1486          $this->assertEquals($this->user->id, $message->useridto);
1487          $this->assertEquals('coursecompleted', $message->eventtype);
1488          $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject);
1489          $this->assertStringContainsString($this->course->fullname, $message->fullmessage);
1490      }
1491  
1492      /**
1493       * Test course completed event.
1494       *
1495       * @covers \core\event\course_completion_updated
1496       */
1497      public function test_course_completion_updated_event() {
1498          $this->setup_data();
1499          $coursecontext = context_course::instance($this->course->id);
1500          $coursecompletionevent = \core\event\course_completion_updated::create(
1501                  array(
1502                      'courseid' => $this->course->id,
1503                      'context' => $coursecontext
1504                      )
1505                  );
1506  
1507          // Mark course as complete and get triggered event.
1508          $sink = $this->redirectEvents();
1509          $coursecompletionevent->trigger();
1510          $events = $sink->get_events();
1511          $event = array_pop($events);
1512          $sink->close();
1513  
1514          $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1515          $this->assertEquals($this->course->id, $event->courseid);
1516          $this->assertEquals($coursecontext, $event->get_context());
1517          $this->assertInstanceOf('moodle_url', $event->get_url());
1518          $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id);
1519          $this->assertEventLegacyLogData($expectedlegacylog, $event);
1520      }
1521  
1522      /**
1523       * @covers \completion_can_view_data
1524       */
1525      public function test_completion_can_view_data() {
1526          $this->setup_data();
1527  
1528          $student = $this->getDataGenerator()->create_user();
1529          $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
1530  
1531          $this->setUser($student);
1532          $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
1533          $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
1534      }
1535  
1536      /**
1537       * Data provider for test_get_grade_completion().
1538       *
1539       * @return array[]
1540       */
1541      public function get_grade_completion_provider() {
1542          return [
1543              'Grade not required' => [false, false, null, null, null],
1544              'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE],
1545              'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE],
1546              'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS],
1547              'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL],
1548          ];
1549      }
1550  
1551      /**
1552       * Test for \completion_info::get_grade_completion().
1553       *
1554       * @dataProvider get_grade_completion_provider
1555       * @param bool $completionusegrade Whether the test activity has grade completion requirement.
1556       * @param bool $hasgrade Whether to set grade for the user in this activity.
1557       * @param int|null $passinggrade Passing grade to set for the test activity.
1558       * @param string|null $expectedexception Expected exception.
1559       * @param int|null $expectedresult The expected completion status.
1560       * @covers ::get_grade_completion
1561       */
1562      public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?int $passinggrade,
1563          ?string $expectedexception, ?int $expectedresult) {
1564          $this->setup_data();
1565  
1566          /** @var \mod_assign_generator $assigngenerator */
1567          $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
1568          $assign = $assigngenerator->create_instance([
1569              'course' => $this->course->id,
1570              'completion' => COMPLETION_ENABLED,
1571              'completionusegrade' => $completionusegrade,
1572              'gradepass' => $passinggrade,
1573          ]);
1574  
1575          $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1576          if ($completionusegrade && $hasgrade) {
1577              $assigninstance = new assign($cm->context, $cm, $this->course);
1578              $grade = $assigninstance->get_user_grade($this->user->id, true);
1579              $grade->grade = 75;
1580              $assigninstance->update_grade($grade);
1581          }
1582  
1583          $completioninfo = new completion_info($this->course);
1584          if ($expectedexception) {
1585              $this->expectException($expectedexception);
1586          }
1587          $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user->id);
1588          $this->assertEquals($expectedresult, $gradecompletion);
1589      }
1590  
1591      /**
1592       * Test the return value for cases when the activity module does not have associated grade_item.
1593       *
1594       * @covers ::get_grade_completion
1595       */
1596      public function test_get_grade_completion_without_grade_item() {
1597          global $DB;
1598  
1599          $this->setup_data();
1600  
1601          $assign = $this->getDataGenerator()->get_plugin_generator('mod_assign')->create_instance([
1602              'course' => $this->course->id,
1603              'completion' => COMPLETION_ENABLED,
1604              'completionusegrade' => true,
1605              'gradepass' => 42,
1606          ]);
1607  
1608          $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1609  
1610          $DB->delete_records('grade_items', [
1611              'courseid' => $this->course->id,
1612              'itemtype' => 'mod',
1613              'itemmodule' => 'assign',
1614              'iteminstance' => $assign->id,
1615          ]);
1616  
1617          // Without the grade_item, the activity is considered incomplete.
1618          $completioninfo = new completion_info($this->course);
1619          $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1620  
1621          // Once the activity is graded, the grade_item is automatically created.
1622          $assigninstance = new assign($cm->context, $cm, $this->course);
1623          $grade = $assigninstance->get_user_grade($this->user->id, true);
1624          $grade->grade = 40;
1625          $assigninstance->update_grade($grade);
1626  
1627          // The implicitly created grade_item does not have grade to pass defined so it is not distinguished.
1628          $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1629      }
1630  
1631      /**
1632       * Test for aggregate_completions().
1633       *
1634       * @covers \aggregate_completions
1635       */
1636      public function test_aggregate_completions() {
1637          global $DB;
1638          $this->resetAfterTest(true);
1639          $time = time();
1640  
1641          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1642  
1643          for ($i = 0; $i < 4; $i++) {
1644              $students[] = $this->getDataGenerator()->create_user();
1645          }
1646  
1647          $teacher = $this->getDataGenerator()->create_user();
1648          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1649          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1650  
1651          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1652          foreach ($students as $student) {
1653              $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1654          }
1655  
1656          $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id),
1657              array('completion' => 1));
1658          $cmdata = get_coursemodule_from_id('data', $data->cmid);
1659  
1660          // Add activity completion criteria.
1661          $criteriadata = new stdClass();
1662          $criteriadata->id = $course->id;
1663          $criteriadata->criteria_activity = array();
1664          // Some activities.
1665          $criteriadata->criteria_activity[$cmdata->id] = 1;
1666          $class = 'completion_criteria_activity';
1667          $criterion = new $class();
1668          $criterion->update_config($criteriadata);
1669  
1670          $this->setUser($teacher);
1671  
1672          // Mark activity complete for both students.
1673          $cm = get_coursemodule_from_instance('data', $data->id);
1674          $completioncriteria = $DB->get_record('course_completion_criteria', []);
1675          foreach ($students as $student) {
1676              $cmcompletionrecords[] = (object)[
1677                  'coursemoduleid' => $cm->id,
1678                  'userid' => $student->id,
1679                  'completionstate' => 1,
1680                  'viewed' => 0,
1681                  'overrideby' => null,
1682                  'timemodified' => 0,
1683              ];
1684  
1685              $usercompletions[] = (object)[
1686                  'criteriaid' => $completioncriteria->id,
1687                  'userid' => $student->id,
1688                  'timecompleted' => $time,
1689              ];
1690  
1691              $cc = array(
1692                  'course'    => $course->id,
1693                  'userid'    => $student->id
1694              );
1695              $ccompletion = new completion_completion($cc);
1696              $completion[] = $ccompletion->mark_inprogress($time);
1697          }
1698          $DB->insert_records('course_modules_completion', $cmcompletionrecords);
1699          $DB->insert_records('course_completion_crit_compl', $usercompletions);
1700  
1701          // MDL-33320: for instant completions we need aggregate to work in a single run.
1702          $DB->set_field('course_completions', 'reaggregate', $time - 2);
1703  
1704          foreach ($students as $student) {
1705              $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]);
1706              $this->assertFalse($result);
1707          }
1708  
1709          aggregate_completions($completion[0]);
1710  
1711          $result1 = $DB->get_record('course_completions', ['userid' => $students[0]->id, 'reaggregate' => 0]);
1712          $result2 = $DB->get_record('course_completions', ['userid' => $students[1]->id, 'reaggregate' => 0]);
1713          $result3 = $DB->get_record('course_completions', ['userid' => $students[2]->id, 'reaggregate' => 0]);
1714  
1715          $this->assertIsObject($result1);
1716          $this->assertFalse($result2);
1717          $this->assertFalse($result3);
1718  
1719          aggregate_completions(0);
1720  
1721          foreach ($students as $student) {
1722              $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]);
1723              $this->assertIsObject($result);
1724          }
1725      }
1726  
1727      /**
1728       * Test for completion_completion::_save().
1729       *
1730       * @covers \completion_completion::_save
1731       */
1732      public function test_save() {
1733          global $DB;
1734          $this->resetAfterTest(true);
1735  
1736          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1737  
1738          $student = $this->getDataGenerator()->create_user();
1739          $teacher = $this->getDataGenerator()->create_user();
1740          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1741          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1742  
1743          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1744          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1745  
1746          $this->setUser($teacher);
1747  
1748          $cc = array(
1749              'course'    => $course->id,
1750              'userid'    => $student->id
1751          );
1752          $ccompletion = new completion_completion($cc);
1753  
1754          $completions = $DB->get_records('course_completions');
1755          $this->assertEmpty($completions);
1756  
1757          // We're testing a private method, so we need to setup reflector magic.
1758          $method = new ReflectionMethod($ccompletion, '_save');
1759          $method->setAccessible(true); // Allow accessing of private method.
1760          $completionid = $method->invoke($ccompletion);
1761          $completions = $DB->get_records('course_completions');
1762          $this->assertEquals(count($completions), 1);
1763          $this->assertEquals(reset($completions)->id, $completionid);
1764  
1765          $ccompletion->id = 0;
1766          $method = new ReflectionMethod($ccompletion, '_save');
1767          $method->setAccessible(true); // Allow accessing of private method.
1768          $completionid = $method->invoke($ccompletion);
1769          $this->assertDebuggingCalled('Can not update data object, no id!');
1770          $this->assertNull($completionid);
1771      }
1772  
1773      /**
1774       * Test for completion_completion::mark_enrolled().
1775       *
1776       * @covers \completion_completion::mark_enrolled
1777       */
1778      public function test_mark_enrolled() {
1779          global $DB;
1780          $this->resetAfterTest(true);
1781  
1782          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1783  
1784          $student = $this->getDataGenerator()->create_user();
1785          $teacher = $this->getDataGenerator()->create_user();
1786          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1787          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1788  
1789          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1790          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1791  
1792          $this->setUser($teacher);
1793  
1794          $cc = array(
1795              'course'    => $course->id,
1796              'userid'    => $student->id
1797          );
1798          $ccompletion = new completion_completion($cc);
1799  
1800          $completions = $DB->get_records('course_completions');
1801          $this->assertEmpty($completions);
1802  
1803          $completionid = $ccompletion->mark_enrolled();
1804          $completions = $DB->get_records('course_completions');
1805          $this->assertEquals(count($completions), 1);
1806          $this->assertEquals(reset($completions)->id, $completionid);
1807  
1808          $ccompletion->id = 0;
1809          $completionid = $ccompletion->mark_enrolled();
1810          $this->assertDebuggingCalled('Can not update data object, no id!');
1811          $this->assertNull($completionid);
1812          $completions = $DB->get_records('course_completions');
1813          $this->assertEquals(1, count($completions));
1814      }
1815  
1816      /**
1817       * Test for completion_completion::mark_inprogress().
1818       *
1819       * @covers \completion_completion::mark_inprogress
1820       */
1821      public function test_mark_inprogress() {
1822          global $DB;
1823          $this->resetAfterTest(true);
1824  
1825          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1826  
1827          $student = $this->getDataGenerator()->create_user();
1828          $teacher = $this->getDataGenerator()->create_user();
1829          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1830          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1831  
1832          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1833          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1834  
1835          $this->setUser($teacher);
1836  
1837          $cc = array(
1838              'course'    => $course->id,
1839              'userid'    => $student->id
1840          );
1841          $ccompletion = new completion_completion($cc);
1842  
1843          $completions = $DB->get_records('course_completions');
1844          $this->assertEmpty($completions);
1845  
1846          $completionid = $ccompletion->mark_inprogress();
1847          $completions = $DB->get_records('course_completions');
1848          $this->assertEquals(1, count($completions));
1849          $this->assertEquals(reset($completions)->id, $completionid);
1850  
1851          $ccompletion->id = 0;
1852          $completionid = $ccompletion->mark_inprogress();
1853          $this->assertDebuggingCalled('Can not update data object, no id!');
1854          $this->assertNull($completionid);
1855          $completions = $DB->get_records('course_completions');
1856          $this->assertEquals(1, count($completions));
1857      }
1858  
1859      /**
1860       * Test for completion_completion::mark_complete().
1861       *
1862       * @covers \completion_completion::mark_complete
1863       */
1864      public function test_mark_complete() {
1865          global $DB;
1866          $this->resetAfterTest(true);
1867  
1868          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1869  
1870          $student = $this->getDataGenerator()->create_user();
1871          $teacher = $this->getDataGenerator()->create_user();
1872          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1873          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1874  
1875          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1876          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1877  
1878          $this->setUser($teacher);
1879  
1880          $cc = array(
1881              'course'    => $course->id,
1882              'userid'    => $student->id
1883          );
1884          $ccompletion = new completion_completion($cc);
1885  
1886          $completions = $DB->get_records('course_completions');
1887          $this->assertEmpty($completions);
1888  
1889          $completionid = $ccompletion->mark_complete();
1890          $completions = $DB->get_records('course_completions');
1891          $this->assertEquals(1, count($completions));
1892          $this->assertEquals(reset($completions)->id, $completionid);
1893  
1894          $ccompletion->id = 0;
1895          $completionid = $ccompletion->mark_complete();
1896          $this->assertNull($completionid);
1897          $completions = $DB->get_records('course_completions');
1898          $this->assertEquals(1, count($completions));
1899      }
1900  
1901      /**
1902       * Test for completion_criteria_completion::mark_complete().
1903       *
1904       * @covers \completion_criteria_completion::mark_complete
1905       */
1906      public function test_criteria_mark_complete() {
1907          global $DB;
1908          $this->resetAfterTest(true);
1909  
1910          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1911  
1912          $student = $this->getDataGenerator()->create_user();
1913          $teacher = $this->getDataGenerator()->create_user();
1914          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1915          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1916  
1917          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1918          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1919  
1920          $this->setUser($teacher);
1921  
1922          $record = [
1923              'course'    => $course->id,
1924              'criteriaid'    => 1,
1925              'userid'    => $student->id,
1926              'timecompleted' => time()
1927          ];
1928          $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY);
1929  
1930          $completions = $DB->get_records('course_completions');
1931          $this->assertEmpty($completions);
1932  
1933          $completionid = $completion->mark_complete($record['timecompleted']);
1934          $completions = $DB->get_records('course_completions');
1935          $this->assertEquals(1, count($completions));
1936          $this->assertEquals(reset($completions)->id, $completionid);
1937      }
1938  }
1939  
1940  class core_completionlib_fake_recordset implements Iterator {
1941      protected $closed;
1942      protected $values, $index;
1943  
1944      public function __construct($values) {
1945          $this->values = $values;
1946          $this->index = 0;
1947      }
1948  
1949      public function current() {
1950          return $this->values[$this->index];
1951      }
1952  
1953      public function key() {
1954          return $this->values[$this->index];
1955      }
1956  
1957      public function next() {
1958          $this->index++;
1959      }
1960  
1961      public function rewind() {
1962          $this->index = 0;
1963      }
1964  
1965      public function valid() {
1966          return count($this->values) > $this->index;
1967      }
1968  
1969      public function close() {
1970          $this->closed = true;
1971      }
1972  
1973      public function was_closed() {
1974          return $this->closed;
1975      }
1976  }