See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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 'overrideby' => null, 760 'timemodified' => 0, 761 ]; 762 $cmcompletionviewrecord = (object)[ 763 'coursemoduleid' => $cm->id, 764 'userid' => $user->id, 765 'timecreated' => 0, 766 ]; 767 $DB->insert_record('course_modules_completion', $cmcompletionrecord); 768 $DB->insert_record('course_modules_viewed', $cmcompletionviewrecord); 769 } 770 771 // Whether we expect for the returned completion data to be stored in the cache. 772 $iscached = true; 773 774 if (!$sameuser) { 775 $iscached = false; 776 $this->setAdminUser(); 777 } else { 778 $this->setUser($user); 779 } 780 781 // Mock other completion data. 782 $completioninfo = new completion_info($this->course); 783 784 $result = $completioninfo->get_data($cm, $wholecourse, $user->id); 785 786 // Course module ID of the returned completion data must match this activity's course module ID. 787 $this->assertEquals($cm->id, $result->coursemoduleid); 788 // User ID of the returned completion data must match the user's ID. 789 $this->assertEquals($user->id, $result->userid); 790 // The completion state of the returned completion data must match the expected completion state. 791 $this->assertEquals($completion, $result->completionstate); 792 793 // If the user has no completion record, then the default record should be returned. 794 if (!$hasrecord) { 795 $this->assertEquals(0, $result->id); 796 } 797 798 // Check that we are including relevant completion data for the module. 799 if (!$wholecourse) { 800 $this->assertTrue(property_exists($result, 'viewed')); 801 $this->assertTrue(property_exists($result, 'customcompletion')); 802 } 803 } 804 805 /** 806 * @covers ::get_data 807 */ 808 public function test_get_data_successive_calls(): void { 809 global $DB; 810 811 $this->setup_data(); 812 $this->setUser($this->user); 813 814 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice'); 815 $choice = $choicegenerator->create_instance([ 816 'course' => $this->course->id, 817 'completion' => COMPLETION_TRACKING_AUTOMATIC, 818 'completionview' => true, 819 'completionsubmit' => true, 820 ]); 821 822 $cm = get_coursemodule_from_instance('choice', $choice->id); 823 824 // Let's manually create a course completion record instead of going through the hoops to complete an activity. 825 $cmcompletionrecord = (object) [ 826 'coursemoduleid' => $cm->id, 827 'userid' => $this->user->id, 828 'completionstate' => COMPLETION_NOT_VIEWED, 829 'overrideby' => null, 830 'timemodified' => 0, 831 ]; 832 $cmcompletionviewrecord = (object)[ 833 'coursemoduleid' => $cm->id, 834 'userid' => $this->user->id, 835 'timecreated' => 0, 836 ]; 837 $DB->insert_record('course_modules_completion', $cmcompletionrecord); 838 $DB->insert_record('course_modules_viewed', $cmcompletionviewrecord); 839 840 // Mock other completion data. 841 $completioninfo = new completion_info($this->course); 842 843 $modinfo = get_fast_modinfo($this->course); 844 $results = []; 845 foreach ($modinfo->cms as $testcm) { 846 $result = $completioninfo->get_data($testcm, true); 847 $this->assertTrue(property_exists($result, 'id')); 848 $this->assertTrue(property_exists($result, 'coursemoduleid')); 849 $this->assertTrue(property_exists($result, 'userid')); 850 $this->assertTrue(property_exists($result, 'completionstate')); 851 $this->assertTrue(property_exists($result, 'viewed')); 852 $this->assertTrue(property_exists($result, 'overrideby')); 853 $this->assertTrue(property_exists($result, 'timemodified')); 854 $this->assertFalse(property_exists($result, 'other_cm_completion_data_fetched')); 855 856 $this->assertEquals($testcm->id, $result->coursemoduleid); 857 $this->assertEquals($this->user->id, $result->userid); 858 859 $results[$testcm->id] = $result; 860 } 861 862 $result = $completioninfo->get_data($cm); 863 $this->assertTrue(property_exists($result, 'customcompletion')); 864 865 // The data should match when fetching modules individually. 866 (cache::make('core', 'completion'))->purge(); 867 foreach ($modinfo->cms as $testcm) { 868 $result = $completioninfo->get_data($testcm, false); 869 $this->assertEquals($result, $results[$testcm->id]); 870 } 871 } 872 873 /** 874 * Tests for get_completion_data(). 875 * 876 * @covers ::get_completion_data 877 */ 878 public function test_get_completion_data() { 879 $this->setup_data(); 880 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice'); 881 $choice = $choicegenerator->create_instance([ 882 'course' => $this->course->id, 883 'completion' => COMPLETION_TRACKING_AUTOMATIC, 884 'completionview' => true, 885 'completionsubmit' => true, 886 ]); 887 $cm = get_coursemodule_from_instance('choice', $choice->id); 888 889 // Mock other completion data. 890 $completioninfo = new completion_info($this->course); 891 // Default data to return when no completion data is found. 892 $defaultdata = [ 893 'id' => 0, 894 'coursemoduleid' => $cm->id, 895 'userid' => $this->user->id, 896 'completionstate' => 0, 897 'viewed' => 0, 898 'overrideby' => null, 899 'timemodified' => 0, 900 ]; 901 902 $completiondatabeforeview = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata); 903 $this->assertTrue(array_key_exists('viewed', $completiondatabeforeview)); 904 $this->assertTrue(array_key_exists('coursemoduleid', $completiondatabeforeview)); 905 $this->assertEquals(0, $completiondatabeforeview['viewed']); 906 $this->assertEquals($cm->id, $completiondatabeforeview['coursemoduleid']); 907 908 // Set viewed. 909 $completioninfo->set_module_viewed($cm, $this->user->id); 910 911 $completiondata = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata); 912 $this->assertTrue(array_key_exists('viewed', $completiondata)); 913 $this->assertTrue(array_key_exists('coursemoduleid', $completiondata)); 914 $this->assertEquals(1, $completiondata['viewed']); 915 $this->assertEquals($cm->id, $completiondatabeforeview['coursemoduleid']); 916 917 $completioninfo->reset_all_state($cm); 918 919 $completiondataafterreset = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata); 920 $this->assertTrue(array_key_exists('viewed', $completiondataafterreset)); 921 $this->assertTrue(array_key_exists('coursemoduleid', $completiondataafterreset)); 922 $this->assertEquals(1, $completiondataafterreset['viewed']); 923 $this->assertEquals($cm->id, $completiondatabeforeview['coursemoduleid']); 924 } 925 926 /** 927 * Tests for completion_info::get_other_cm_completion_data(). 928 * 929 * @covers ::get_other_cm_completion_data 930 */ 931 public function test_get_other_cm_completion_data() { 932 global $DB; 933 934 $this->setup_data(); 935 $user = $this->user; 936 937 $this->setAdminUser(); 938 939 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice'); 940 $choice = $choicegenerator->create_instance([ 941 'course' => $this->course->id, 942 'completion' => COMPLETION_TRACKING_AUTOMATIC, 943 'completionsubmit' => true, 944 ]); 945 946 $cmchoice = cm_info::create(get_coursemodule_from_instance('choice', $choice->id)); 947 948 $choice2 = $choicegenerator->create_instance([ 949 'course' => $this->course->id, 950 'completion' => COMPLETION_TRACKING_AUTOMATIC, 951 ]); 952 953 $cmchoice2 = cm_info::create(get_coursemodule_from_instance('choice', $choice2->id)); 954 955 $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop'); 956 $workshop = $workshopgenerator->create_instance([ 957 'course' => $this->course->id, 958 'completion' => COMPLETION_TRACKING_AUTOMATIC, 959 // Submission grade required. 960 'completiongradeitemnumber' => 0, 961 'completionpassgrade' => 1, 962 ]); 963 964 $cmworkshop = cm_info::create(get_coursemodule_from_instance('workshop', $workshop->id)); 965 966 $completioninfo = new completion_info($this->course); 967 968 $method = new ReflectionMethod("completion_info", "get_other_cm_completion_data"); 969 $method->setAccessible(true); 970 971 // Check that fetching data for a module with custom completion provides its info. 972 $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id); 973 974 $this->assertArrayHasKey('customcompletion', $choicecompletiondata); 975 $this->assertArrayHasKey('completionsubmit', $choicecompletiondata['customcompletion']); 976 $this->assertEquals(COMPLETION_INCOMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']); 977 978 // Mock a choice answer so user has completed the requirement. 979 $choicemockinfo = [ 980 'choiceid' => $cmchoice->instance, 981 'userid' => $this->user->id 982 ]; 983 $DB->insert_record('choice_answers', $choicemockinfo, false); 984 985 // Confirm fetching again reflects the completion. 986 $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id); 987 $this->assertEquals(COMPLETION_COMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']); 988 989 // Check that fetching data for a module with no custom completion still provides its grade completion status. 990 $workshopcompletiondata = $method->invoke($completioninfo, $cmworkshop, $user->id); 991 992 $this->assertArrayHasKey('completiongrade', $workshopcompletiondata); 993 $this->assertArrayHasKey('passgrade', $workshopcompletiondata); 994 $this->assertArrayNotHasKey('customcompletion', $workshopcompletiondata); 995 $this->assertEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['completiongrade']); 996 $this->assertEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['passgrade']); 997 998 // Check that fetching data for a module with no completion conditions does not provide any data. 999 $choice2completiondata = $method->invoke($completioninfo, $cmchoice2, $user->id); 1000 $this->assertEmpty($choice2completiondata); 1001 } 1002 1003 /** 1004 * @covers ::internal_set_data 1005 */ 1006 public function test_internal_set_data() { 1007 global $DB; 1008 $this->setup_data(); 1009 1010 $this->setUser($this->user); 1011 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 1012 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto); 1013 $cm = get_coursemodule_from_instance('forum', $forum->id); 1014 $c = new completion_info($this->course); 1015 1016 // 1) Test with new data. 1017 $data = new stdClass(); 1018 $data->id = 0; 1019 $data->userid = $this->user->id; 1020 $data->coursemoduleid = $cm->id; 1021 $data->completionstate = COMPLETION_COMPLETE; 1022 $data->timemodified = time(); 1023 $data->viewed = COMPLETION_NOT_VIEWED; 1024 $data->overrideby = null; 1025 1026 $c->internal_set_data($cm, $data); 1027 $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id)); 1028 $this->assertEquals($d1, $data->id); 1029 $cache = cache::make('core', 'completion'); 1030 // Cache was not set for another user. 1031 $cachevalue = $cache->get("{$data->userid}_{$cm->course}"); 1032 $this->assertEquals([ 1033 'cacherev' => $this->course->cacherev, 1034 $cm->id => array_merge( 1035 (array) $data, 1036 ['other_cm_completion_data_fetched' => true] 1037 ), 1038 ], 1039 $cachevalue); 1040 1041 // 2) Test with existing data and for different user. 1042 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto); 1043 $cm2 = get_coursemodule_from_instance('forum', $forum2->id); 1044 $newuser = $this->getDataGenerator()->create_user(); 1045 1046 $d2 = new stdClass(); 1047 $d2->id = 7; 1048 $d2->userid = $newuser->id; 1049 $d2->coursemoduleid = $cm2->id; 1050 $d2->completionstate = COMPLETION_COMPLETE; 1051 $d2->timemodified = time(); 1052 $d2->viewed = COMPLETION_NOT_VIEWED; 1053 $d2->overrideby = null; 1054 $c->internal_set_data($cm2, $d2); 1055 // Cache for current user returns the data. 1056 $cachevalue = $cache->get($data->userid . '_' . $cm->course); 1057 $this->assertEquals(array_merge( 1058 (array) $data, 1059 ['other_cm_completion_data_fetched' => true] 1060 ), $cachevalue[$cm->id]); 1061 1062 // Cache for another user is not filled. 1063 $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course)); 1064 1065 // 3) Test where it THINKS the data is new (from cache) but actually in the database it has been set since. 1066 $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto); 1067 $cm3 = get_coursemodule_from_instance('forum', $forum3->id); 1068 $newuser2 = $this->getDataGenerator()->create_user(); 1069 $d3 = new stdClass(); 1070 $d3->id = 13; 1071 $d3->userid = $newuser2->id; 1072 $d3->coursemoduleid = $cm3->id; 1073 $d3->completionstate = COMPLETION_COMPLETE; 1074 $d3->timemodified = time(); 1075 $d3->viewed = COMPLETION_NOT_VIEWED; 1076 $d3->overrideby = null; 1077 $DB->insert_record('course_modules_completion', $d3); 1078 $c->internal_set_data($cm, $data); 1079 1080 // 4) Test instant course completions. 1081 $dataactivity = $this->getDataGenerator()->create_module('data', array('course' => $this->course->id), 1082 array('completion' => 1)); 1083 $cm = get_coursemodule_from_instance('data', $dataactivity->id); 1084 $c = new completion_info($this->course); 1085 $cmdata = get_coursemodule_from_id('data', $dataactivity->cmid); 1086 1087 // Add activity completion criteria. 1088 $criteriadata = new stdClass(); 1089 $criteriadata->id = $this->course->id; 1090 $criteriadata->criteria_activity = array(); 1091 // Some activities. 1092 $criteriadata->criteria_activity[$cmdata->id] = 1; 1093 $class = 'completion_criteria_activity'; 1094 $criterion = new $class(); 1095 $criterion->update_config($criteriadata); 1096 1097 $actual = $DB->get_records('course_completions'); 1098 $this->assertEmpty($actual); 1099 1100 $data->coursemoduleid = $cm->id; 1101 $c->internal_set_data($cm, $data); 1102 $actual = $DB->get_records('course_completions'); 1103 $this->assertEquals(1, count($actual)); 1104 $this->assertEquals($this->user->id, reset($actual)->userid); 1105 1106 $data->userid = $newuser2->id; 1107 $c->internal_set_data($cm, $data, true); 1108 $actual = $DB->get_records('course_completions'); 1109 $this->assertEquals(1, count($actual)); 1110 $this->assertEquals($this->user->id, reset($actual)->userid); 1111 } 1112 1113 /** 1114 * @covers ::get_progress_all 1115 */ 1116 public function test_get_progress_all_few() { 1117 global $DB; 1118 $this->mock_setup(); 1119 1120 $mockbuilder = $this->getMockBuilder('completion_info'); 1121 $mockbuilder->onlyMethods(array('get_tracked_users')); 1122 $mockbuilder->setConstructorArgs(array((object)array('id' => 42))); 1123 $c = $mockbuilder->getMock(); 1124 1125 // With few results. 1126 $c->expects($this->once()) 1127 ->method('get_tracked_users') 1128 ->with(false, array(), 0, '', '', '', null) 1129 ->will($this->returnValue(array( 1130 (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'), 1131 (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy')))); 1132 $DB->expects($this->once()) 1133 ->method('get_in_or_equal') 1134 ->with(array(100, 201)) 1135 ->will($this->returnValue(array(' IN (100, 201)', array()))); 1136 $progress1 = (object)array('userid' => 100, 'coursemoduleid' => 13); 1137 $progress2 = (object)array('userid' => 201, 'coursemoduleid' => 14); 1138 $DB->expects($this->once()) 1139 ->method('get_recordset_sql') 1140 ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2)))); 1141 1142 $this->assertEquals(array( 1143 100 => (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh', 1144 'progress' => array(13 => $progress1)), 1145 201 => (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy', 1146 'progress' => array(14 => $progress2)), 1147 ), $c->get_progress_all(false)); 1148 } 1149 1150 /** 1151 * @covers ::get_progress_all 1152 */ 1153 public function test_get_progress_all_lots() { 1154 global $DB; 1155 $this->mock_setup(); 1156 1157 $mockbuilder = $this->getMockBuilder('completion_info'); 1158 $mockbuilder->onlyMethods(array('get_tracked_users')); 1159 $mockbuilder->setConstructorArgs(array((object)array('id' => 42))); 1160 $c = $mockbuilder->getMock(); 1161 1162 $tracked = array(); 1163 $ids = array(); 1164 $progress = array(); 1165 // With more than 1000 results. 1166 for ($i = 100; $i < 2000; $i++) { 1167 $tracked[] = (object)array('id' => $i, 'firstname' => 'frog', 'lastname' => $i); 1168 $ids[] = $i; 1169 $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 13); 1170 $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 14); 1171 } 1172 $c->expects($this->once()) 1173 ->method('get_tracked_users') 1174 ->with(true, 3, 0, '', '', '', null) 1175 ->will($this->returnValue($tracked)); 1176 $DB->expects($this->exactly(2)) 1177 ->method('get_in_or_equal') 1178 ->withConsecutive( 1179 array(array_slice($ids, 0, 1000)), 1180 array(array_slice($ids, 1000)) 1181 ) 1182 ->willReturnOnConsecutiveCalls( 1183 array(' IN whatever', array()), 1184 array(' IN whatever2', array())); 1185 $DB->expects($this->exactly(2)) 1186 ->method('get_recordset_sql') 1187 ->willReturnOnConsecutiveCalls( 1188 new core_completionlib_fake_recordset(array_slice($progress, 0, 1000)), 1189 new core_completionlib_fake_recordset(array_slice($progress, 1000))); 1190 1191 $result = $c->get_progress_all(true, 3); 1192 $resultok = true; 1193 $resultok = $resultok && ($ids == array_keys($result)); 1194 1195 foreach ($result as $userid => $data) { 1196 $resultok = $resultok && $data->firstname == 'frog'; 1197 $resultok = $resultok && $data->lastname == $userid; 1198 $resultok = $resultok && $data->id == $userid; 1199 $cms = $data->progress; 1200 $resultok = $resultok && (array(13, 14) == array_keys($cms)); 1201 $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 13) == $cms[13]); 1202 $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 14) == $cms[14]); 1203 } 1204 $this->assertTrue($resultok); 1205 $this->assertCount(count($tracked), $result); 1206 } 1207 1208 /** 1209 * @covers ::inform_grade_changed 1210 */ 1211 public function test_inform_grade_changed() { 1212 $this->mock_setup(); 1213 1214 $mockbuilder = $this->getMockBuilder('completion_info'); 1215 $mockbuilder->onlyMethods(array('is_enabled', 'update_state')); 1216 $mockbuilder->setConstructorArgs(array((object)array('id' => 42))); 1217 1218 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => null); 1219 $item = (object)array('itemnumber' => 3, 'gradepass' => 1, 'hidden' => 0); 1220 $grade = (object)array('userid' => 31337, 'finalgrade' => 0, 'rawgrade' => 0); 1221 1222 // Not enabled (should do nothing). 1223 $c = $mockbuilder->getMock(); 1224 $c->expects($this->once()) 1225 ->method('is_enabled') 1226 ->with($cm) 1227 ->will($this->returnValue(false)); 1228 $c->inform_grade_changed($cm, $item, $grade, false); 1229 1230 // Enabled but still no grade completion required, should still do nothing. 1231 $c = $mockbuilder->getMock(); 1232 $c->expects($this->once()) 1233 ->method('is_enabled') 1234 ->with($cm) 1235 ->will($this->returnValue(true)); 1236 $c->inform_grade_changed($cm, $item, $grade, false); 1237 1238 // Enabled and completion required but item number is wrong, does nothing. 1239 $c = $mockbuilder->getMock(); 1240 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 7); 1241 $c->expects($this->once()) 1242 ->method('is_enabled') 1243 ->with($cm) 1244 ->will($this->returnValue(true)); 1245 $c->inform_grade_changed($cm, $item, $grade, false); 1246 1247 // Enabled and completion required and item number right. It is supposed 1248 // to call update_state with the new potential state being obtained from 1249 // internal_get_grade_state. 1250 $c = $mockbuilder->getMock(); 1251 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3); 1252 $grade = (object)array('userid' => 31337, 'finalgrade' => 1, 'rawgrade' => 0); 1253 $c->expects($this->once()) 1254 ->method('is_enabled') 1255 ->with($cm) 1256 ->will($this->returnValue(true)); 1257 $c->expects($this->once()) 1258 ->method('update_state') 1259 ->with($cm, COMPLETION_COMPLETE_PASS, 31337) 1260 ->will($this->returnValue(true)); 1261 $c->inform_grade_changed($cm, $item, $grade, false); 1262 1263 // Same as above but marked deleted. It is supposed to call update_state 1264 // with new potential state being COMPLETION_INCOMPLETE. 1265 $c = $mockbuilder->getMock(); 1266 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3); 1267 $grade = (object)array('userid' => 31337, 'finalgrade' => 1, 'rawgrade' => 0); 1268 $c->expects($this->once()) 1269 ->method('is_enabled') 1270 ->with($cm) 1271 ->will($this->returnValue(true)); 1272 $c->expects($this->once()) 1273 ->method('update_state') 1274 ->with($cm, COMPLETION_INCOMPLETE, 31337) 1275 ->will($this->returnValue(true)); 1276 $c->inform_grade_changed($cm, $item, $grade, true); 1277 } 1278 1279 /** 1280 * @covers ::internal_get_grade_state 1281 */ 1282 public function test_internal_get_grade_state() { 1283 $this->mock_setup(); 1284 1285 $item = new stdClass; 1286 $grade = new stdClass; 1287 1288 $item->gradepass = 4; 1289 $item->hidden = 0; 1290 $grade->rawgrade = 4.0; 1291 $grade->finalgrade = null; 1292 1293 // Grade has pass mark and is not hidden, user passes. 1294 $this->assertEquals( 1295 COMPLETION_COMPLETE_PASS, 1296 completion_info::internal_get_grade_state($item, $grade)); 1297 1298 // Same but user fails. 1299 $grade->rawgrade = 3.9; 1300 $this->assertEquals( 1301 COMPLETION_COMPLETE_FAIL, 1302 completion_info::internal_get_grade_state($item, $grade)); 1303 1304 // User fails on raw grade but passes on final. 1305 $grade->finalgrade = 4.0; 1306 $this->assertEquals( 1307 COMPLETION_COMPLETE_PASS, 1308 completion_info::internal_get_grade_state($item, $grade)); 1309 1310 // Item is hidden. 1311 $item->hidden = 1; 1312 $this->assertEquals( 1313 COMPLETION_COMPLETE, 1314 completion_info::internal_get_grade_state($item, $grade)); 1315 1316 // Item isn't hidden but has no pass mark. 1317 $item->hidden = 0; 1318 $item->gradepass = 0; 1319 $this->assertEquals( 1320 COMPLETION_COMPLETE, 1321 completion_info::internal_get_grade_state($item, $grade)); 1322 1323 // Item is hidden, but returnpassfail is true and the grade is passing. 1324 $item->hidden = 1; 1325 $item->gradepass = 4; 1326 $grade->finalgrade = 5.0; 1327 $this->assertEquals( 1328 COMPLETION_COMPLETE_PASS, 1329 completion_info::internal_get_grade_state($item, $grade, true)); 1330 1331 // Item is hidden, but returnpassfail is true and the grade is failing. 1332 $item->hidden = 1; 1333 $item->gradepass = 4; 1334 $grade->finalgrade = 3.0; 1335 $this->assertEquals( 1336 COMPLETION_COMPLETE_FAIL_HIDDEN, 1337 completion_info::internal_get_grade_state($item, $grade, true)); 1338 1339 // Item is not hidden, but returnpassfail is true and the grade is failing. 1340 $item->hidden = 0; 1341 $item->gradepass = 4; 1342 $grade->finalgrade = 3.0; 1343 $this->assertEquals( 1344 COMPLETION_COMPLETE_FAIL, 1345 completion_info::internal_get_grade_state($item, $grade, true)); 1346 } 1347 1348 /** 1349 * @test ::get_activities 1350 */ 1351 public function test_get_activities() { 1352 global $CFG; 1353 $this->resetAfterTest(); 1354 1355 // Enable completion before creating modules, otherwise the completion data is not written in DB. 1356 $CFG->enablecompletion = true; 1357 1358 // Create a course with mixed auto completion data. 1359 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true)); 1360 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 1361 $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL); 1362 $completionnone = array('completion' => COMPLETION_TRACKING_NONE); 1363 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto); 1364 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto); 1365 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual); 1366 1367 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone); 1368 $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone); 1369 $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone); 1370 1371 // Create data in another course to make sure it's not considered. 1372 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true)); 1373 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto); 1374 $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual); 1375 $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone); 1376 1377 $c = new completion_info($course); 1378 $activities = $c->get_activities(); 1379 $this->assertCount(3, $activities); 1380 $this->assertTrue(isset($activities[$forum->cmid])); 1381 $this->assertSame($forum->name, $activities[$forum->cmid]->name); 1382 $this->assertTrue(isset($activities[$page->cmid])); 1383 $this->assertSame($page->name, $activities[$page->cmid]->name); 1384 $this->assertTrue(isset($activities[$data->cmid])); 1385 $this->assertSame($data->name, $activities[$data->cmid]->name); 1386 1387 $this->assertFalse(isset($activities[$forum2->cmid])); 1388 $this->assertFalse(isset($activities[$page2->cmid])); 1389 $this->assertFalse(isset($activities[$data2->cmid])); 1390 } 1391 1392 /** 1393 * @test ::has_activities 1394 */ 1395 public function test_has_activities() { 1396 global $CFG; 1397 $this->resetAfterTest(); 1398 1399 // Enable completion before creating modules, otherwise the completion data is not written in DB. 1400 $CFG->enablecompletion = true; 1401 1402 // Create a course with mixed auto completion data. 1403 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true)); 1404 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true)); 1405 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 1406 $completionnone = array('completion' => COMPLETION_TRACKING_NONE); 1407 $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto); 1408 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone); 1409 1410 $c1 = new completion_info($course); 1411 $c2 = new completion_info($course2); 1412 1413 $this->assertTrue($c1->has_activities()); 1414 $this->assertFalse($c2->has_activities()); 1415 } 1416 1417 /** 1418 * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses 1419 * 1420 * @covers ::delete_course_completion_data 1421 * @covers ::delete_all_completion_data 1422 */ 1423 public function test_course_delete_prerequisite() { 1424 global $DB; 1425 1426 $this->setup_data(); 1427 1428 $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]); 1429 1430 $criteriadata = (object) [ 1431 'id' => $this->course->id, 1432 'criteria_course' => [$courseprerequisite->id], 1433 ]; 1434 1435 /** @var completion_criteria_course $criteria */ 1436 $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]); 1437 $criteria->update_config($criteriadata); 1438 1439 // Sanity test. 1440 $this->assertTrue($DB->record_exists('course_completion_criteria', [ 1441 'course' => $this->course->id, 1442 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE, 1443 'courseinstance' => $courseprerequisite->id, 1444 ])); 1445 1446 // Deleting the prerequisite course should remove the completion criteria. 1447 delete_course($courseprerequisite, false); 1448 1449 $this->assertFalse($DB->record_exists('course_completion_criteria', [ 1450 'course' => $this->course->id, 1451 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE, 1452 'courseinstance' => $courseprerequisite->id, 1453 ])); 1454 } 1455 1456 /** 1457 * Test course module completion update event. 1458 * 1459 * @covers \core\event\course_module_completion_updated 1460 */ 1461 public function test_course_module_completion_updated_event() { 1462 global $USER, $CFG; 1463 1464 $this->setup_data(); 1465 1466 $this->setAdminUser(); 1467 1468 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 1469 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto); 1470 1471 $c = new completion_info($this->course); 1472 $activities = $c->get_activities(); 1473 $this->assertEquals(1, count($activities)); 1474 $this->assertTrue(isset($activities[$forum->cmid])); 1475 $this->assertEquals($activities[$forum->cmid]->name, $forum->name); 1476 1477 $current = $c->get_data($activities[$forum->cmid], false, $this->user->id); 1478 $current->completionstate = COMPLETION_COMPLETE; 1479 $current->timemodified = time(); 1480 $sink = $this->redirectEvents(); 1481 $c->internal_set_data($activities[$forum->cmid], $current); 1482 $events = $sink->get_events(); 1483 $event = reset($events); 1484 $this->assertInstanceOf('\core\event\course_module_completion_updated', $event); 1485 $this->assertEquals($forum->cmid, 1486 $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid); 1487 $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid)); 1488 $this->assertEquals(context_module::instance($forum->cmid), $event->get_context()); 1489 $this->assertEquals($USER->id, $event->userid); 1490 $this->assertEquals($this->user->id, $event->relateduserid); 1491 $this->assertInstanceOf('moodle_url', $event->get_url()); 1492 $this->assertEventLegacyData($current, $event); 1493 } 1494 1495 /** 1496 * Test course completed event. 1497 * 1498 * @covers \core\event\course_completed 1499 */ 1500 public function test_course_completed_event() { 1501 global $USER; 1502 1503 $this->setup_data(); 1504 $this->setAdminUser(); 1505 1506 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 1507 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id)); 1508 1509 // Mark course as complete and get triggered event. 1510 $sink = $this->redirectEvents(); 1511 $ccompletion->mark_complete(); 1512 $events = $sink->get_events(); 1513 $event = reset($events); 1514 1515 $this->assertInstanceOf('\core\event\course_completed', $event); 1516 $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course); 1517 $this->assertEquals($this->course->id, $event->courseid); 1518 $this->assertEquals($USER->id, $event->userid); 1519 $this->assertEquals($this->user->id, $event->relateduserid); 1520 $this->assertEquals(context_course::instance($this->course->id), $event->get_context()); 1521 $this->assertInstanceOf('moodle_url', $event->get_url()); 1522 $data = $ccompletion->get_record_data(); 1523 $this->assertEventLegacyData($data, $event); 1524 } 1525 1526 /** 1527 * Test course completed message. 1528 * 1529 * @covers \core\event\course_completed 1530 */ 1531 public function test_course_completed_message() { 1532 $this->setup_data(); 1533 $this->setAdminUser(); 1534 1535 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 1536 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id)); 1537 1538 // Mark course as complete and get the message. 1539 $sink = $this->redirectMessages(); 1540 $ccompletion->mark_complete(); 1541 $messages = $sink->get_messages(); 1542 $sink->close(); 1543 1544 $this->assertCount(1, $messages); 1545 $message = array_pop($messages); 1546 1547 $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom); 1548 $this->assertEquals($this->user->id, $message->useridto); 1549 $this->assertEquals('coursecompleted', $message->eventtype); 1550 $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject); 1551 $this->assertStringContainsString($this->course->fullname, $message->fullmessage); 1552 } 1553 1554 /** 1555 * Test course completed event. 1556 * 1557 * @covers \core\event\course_completion_updated 1558 */ 1559 public function test_course_completion_updated_event() { 1560 $this->setup_data(); 1561 $coursecontext = context_course::instance($this->course->id); 1562 $coursecompletionevent = \core\event\course_completion_updated::create( 1563 array( 1564 'courseid' => $this->course->id, 1565 'context' => $coursecontext 1566 ) 1567 ); 1568 1569 // Mark course as complete and get triggered event. 1570 $sink = $this->redirectEvents(); 1571 $coursecompletionevent->trigger(); 1572 $events = $sink->get_events(); 1573 $event = array_pop($events); 1574 $sink->close(); 1575 1576 $this->assertInstanceOf('\core\event\course_completion_updated', $event); 1577 $this->assertEquals($this->course->id, $event->courseid); 1578 $this->assertEquals($coursecontext, $event->get_context()); 1579 $this->assertInstanceOf('moodle_url', $event->get_url()); 1580 $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id); 1581 $this->assertEventLegacyLogData($expectedlegacylog, $event); 1582 } 1583 1584 /** 1585 * @covers \completion_can_view_data 1586 */ 1587 public function test_completion_can_view_data() { 1588 $this->setup_data(); 1589 1590 $student = $this->getDataGenerator()->create_user(); 1591 $this->getDataGenerator()->enrol_user($student->id, $this->course->id); 1592 1593 $this->setUser($student); 1594 $this->assertTrue(completion_can_view_data($student->id, $this->course->id)); 1595 $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id)); 1596 } 1597 1598 /** 1599 * Data provider for test_get_grade_completion(). 1600 * 1601 * @return array[] 1602 */ 1603 public function get_grade_completion_provider() { 1604 return [ 1605 'Grade not required' => [false, false, null, null, null], 1606 'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE], 1607 'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE], 1608 'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS], 1609 'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL], 1610 ]; 1611 } 1612 1613 /** 1614 * Test for \completion_info::get_grade_completion(). 1615 * 1616 * @dataProvider get_grade_completion_provider 1617 * @param bool $completionusegrade Whether the test activity has grade completion requirement. 1618 * @param bool $hasgrade Whether to set grade for the user in this activity. 1619 * @param int|null $passinggrade Passing grade to set for the test activity. 1620 * @param string|null $expectedexception Expected exception. 1621 * @param int|null $expectedresult The expected completion status. 1622 * @covers ::get_grade_completion 1623 */ 1624 public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?int $passinggrade, 1625 ?string $expectedexception, ?int $expectedresult) { 1626 $this->setup_data(); 1627 1628 /** @var \mod_assign_generator $assigngenerator */ 1629 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); 1630 $assign = $assigngenerator->create_instance([ 1631 'course' => $this->course->id, 1632 'completion' => COMPLETION_ENABLED, 1633 'completionusegrade' => $completionusegrade, 1634 'gradepass' => $passinggrade, 1635 ]); 1636 1637 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id)); 1638 if ($completionusegrade && $hasgrade) { 1639 $assigninstance = new assign($cm->context, $cm, $this->course); 1640 $grade = $assigninstance->get_user_grade($this->user->id, true); 1641 $grade->grade = 75; 1642 $assigninstance->update_grade($grade); 1643 } 1644 1645 $completioninfo = new completion_info($this->course); 1646 if ($expectedexception) { 1647 $this->expectException($expectedexception); 1648 } 1649 $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user->id); 1650 $this->assertEquals($expectedresult, $gradecompletion); 1651 } 1652 1653 /** 1654 * Test the return value for cases when the activity module does not have associated grade_item. 1655 * 1656 * @covers ::get_grade_completion 1657 */ 1658 public function test_get_grade_completion_without_grade_item() { 1659 global $DB; 1660 1661 $this->setup_data(); 1662 1663 $assign = $this->getDataGenerator()->get_plugin_generator('mod_assign')->create_instance([ 1664 'course' => $this->course->id, 1665 'completion' => COMPLETION_ENABLED, 1666 'completionusegrade' => true, 1667 'gradepass' => 42, 1668 ]); 1669 1670 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id)); 1671 1672 $DB->delete_records('grade_items', [ 1673 'courseid' => $this->course->id, 1674 'itemtype' => 'mod', 1675 'itemmodule' => 'assign', 1676 'iteminstance' => $assign->id, 1677 ]); 1678 1679 // Without the grade_item, the activity is considered incomplete. 1680 $completioninfo = new completion_info($this->course); 1681 $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id)); 1682 1683 // Once the activity is graded, the grade_item is automatically created. 1684 $assigninstance = new assign($cm->context, $cm, $this->course); 1685 $grade = $assigninstance->get_user_grade($this->user->id, true); 1686 $grade->grade = 40; 1687 $assigninstance->update_grade($grade); 1688 1689 // The implicitly created grade_item does not have grade to pass defined so it is not distinguished. 1690 $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id)); 1691 } 1692 1693 /** 1694 * Test for aggregate_completions(). 1695 * 1696 * @covers \aggregate_completions 1697 */ 1698 public function test_aggregate_completions() { 1699 global $DB; 1700 $this->resetAfterTest(true); 1701 $time = time(); 1702 1703 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 1704 1705 for ($i = 0; $i < 4; $i++) { 1706 $students[] = $this->getDataGenerator()->create_user(); 1707 } 1708 1709 $teacher = $this->getDataGenerator()->create_user(); 1710 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 1711 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 1712 1713 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 1714 foreach ($students as $student) { 1715 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 1716 } 1717 1718 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), 1719 array('completion' => 1)); 1720 $cmdata = get_coursemodule_from_id('data', $data->cmid); 1721 1722 // Add activity completion criteria. 1723 $criteriadata = new stdClass(); 1724 $criteriadata->id = $course->id; 1725 $criteriadata->criteria_activity = array(); 1726 // Some activities. 1727 $criteriadata->criteria_activity[$cmdata->id] = 1; 1728 $class = 'completion_criteria_activity'; 1729 $criterion = new $class(); 1730 $criterion->update_config($criteriadata); 1731 1732 $this->setUser($teacher); 1733 1734 // Mark activity complete for both students. 1735 $cm = get_coursemodule_from_instance('data', $data->id); 1736 $completioncriteria = $DB->get_record('course_completion_criteria', []); 1737 foreach ($students as $student) { 1738 $cmcompletionrecords[] = (object)[ 1739 'coursemoduleid' => $cm->id, 1740 'userid' => $student->id, 1741 'completionstate' => 1, 1742 'viewed' => 0, 1743 'overrideby' => null, 1744 'timemodified' => 0, 1745 ]; 1746 1747 $usercompletions[] = (object)[ 1748 'criteriaid' => $completioncriteria->id, 1749 'userid' => $student->id, 1750 'timecompleted' => $time, 1751 ]; 1752 1753 $cc = array( 1754 'course' => $course->id, 1755 'userid' => $student->id 1756 ); 1757 $ccompletion = new completion_completion($cc); 1758 $completion[] = $ccompletion->mark_inprogress($time); 1759 } 1760 $DB->insert_records('course_modules_completion', $cmcompletionrecords); 1761 $DB->insert_records('course_completion_crit_compl', $usercompletions); 1762 1763 // MDL-33320: for instant completions we need aggregate to work in a single run. 1764 $DB->set_field('course_completions', 'reaggregate', $time - 2); 1765 1766 foreach ($students as $student) { 1767 $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]); 1768 $this->assertFalse($result); 1769 } 1770 1771 aggregate_completions($completion[0]); 1772 1773 $result1 = $DB->get_record('course_completions', ['userid' => $students[0]->id, 'reaggregate' => 0]); 1774 $result2 = $DB->get_record('course_completions', ['userid' => $students[1]->id, 'reaggregate' => 0]); 1775 $result3 = $DB->get_record('course_completions', ['userid' => $students[2]->id, 'reaggregate' => 0]); 1776 1777 $this->assertIsObject($result1); 1778 $this->assertFalse($result2); 1779 $this->assertFalse($result3); 1780 1781 aggregate_completions(0); 1782 1783 foreach ($students as $student) { 1784 $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]); 1785 $this->assertIsObject($result); 1786 } 1787 } 1788 1789 /** 1790 * Test for completion_completion::_save(). 1791 * 1792 * @covers \completion_completion::_save 1793 */ 1794 public function test_save() { 1795 global $DB; 1796 $this->resetAfterTest(true); 1797 1798 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 1799 1800 $student = $this->getDataGenerator()->create_user(); 1801 $teacher = $this->getDataGenerator()->create_user(); 1802 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 1803 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 1804 1805 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 1806 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 1807 1808 $this->setUser($teacher); 1809 1810 $cc = array( 1811 'course' => $course->id, 1812 'userid' => $student->id 1813 ); 1814 $ccompletion = new completion_completion($cc); 1815 1816 $completions = $DB->get_records('course_completions'); 1817 $this->assertEmpty($completions); 1818 1819 // We're testing a private method, so we need to setup reflector magic. 1820 $method = new ReflectionMethod($ccompletion, '_save'); 1821 $method->setAccessible(true); // Allow accessing of private method. 1822 $completionid = $method->invoke($ccompletion); 1823 $completions = $DB->get_records('course_completions'); 1824 $this->assertEquals(count($completions), 1); 1825 $this->assertEquals(reset($completions)->id, $completionid); 1826 1827 $ccompletion->id = 0; 1828 $method = new ReflectionMethod($ccompletion, '_save'); 1829 $method->setAccessible(true); // Allow accessing of private method. 1830 $completionid = $method->invoke($ccompletion); 1831 $this->assertDebuggingCalled('Can not update data object, no id!'); 1832 $this->assertNull($completionid); 1833 } 1834 1835 /** 1836 * Test for completion_completion::mark_enrolled(). 1837 * 1838 * @covers \completion_completion::mark_enrolled 1839 */ 1840 public function test_mark_enrolled() { 1841 global $DB; 1842 $this->resetAfterTest(true); 1843 1844 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 1845 1846 $student = $this->getDataGenerator()->create_user(); 1847 $teacher = $this->getDataGenerator()->create_user(); 1848 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 1849 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 1850 1851 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 1852 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 1853 1854 $this->setUser($teacher); 1855 1856 $cc = array( 1857 'course' => $course->id, 1858 'userid' => $student->id 1859 ); 1860 $ccompletion = new completion_completion($cc); 1861 1862 $completions = $DB->get_records('course_completions'); 1863 $this->assertEmpty($completions); 1864 1865 $completionid = $ccompletion->mark_enrolled(); 1866 $completions = $DB->get_records('course_completions'); 1867 $this->assertEquals(count($completions), 1); 1868 $this->assertEquals(reset($completions)->id, $completionid); 1869 1870 $ccompletion->id = 0; 1871 $completionid = $ccompletion->mark_enrolled(); 1872 $this->assertDebuggingCalled('Can not update data object, no id!'); 1873 $this->assertNull($completionid); 1874 $completions = $DB->get_records('course_completions'); 1875 $this->assertEquals(1, count($completions)); 1876 } 1877 1878 /** 1879 * Test for completion_completion::mark_inprogress(). 1880 * 1881 * @covers \completion_completion::mark_inprogress 1882 */ 1883 public function test_mark_inprogress() { 1884 global $DB; 1885 $this->resetAfterTest(true); 1886 1887 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 1888 1889 $student = $this->getDataGenerator()->create_user(); 1890 $teacher = $this->getDataGenerator()->create_user(); 1891 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 1892 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 1893 1894 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 1895 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 1896 1897 $this->setUser($teacher); 1898 1899 $cc = array( 1900 'course' => $course->id, 1901 'userid' => $student->id 1902 ); 1903 $ccompletion = new completion_completion($cc); 1904 1905 $completions = $DB->get_records('course_completions'); 1906 $this->assertEmpty($completions); 1907 1908 $completionid = $ccompletion->mark_inprogress(); 1909 $completions = $DB->get_records('course_completions'); 1910 $this->assertEquals(1, count($completions)); 1911 $this->assertEquals(reset($completions)->id, $completionid); 1912 1913 $ccompletion->id = 0; 1914 $completionid = $ccompletion->mark_inprogress(); 1915 $this->assertDebuggingCalled('Can not update data object, no id!'); 1916 $this->assertNull($completionid); 1917 $completions = $DB->get_records('course_completions'); 1918 $this->assertEquals(1, count($completions)); 1919 } 1920 1921 /** 1922 * Test for completion_completion::mark_complete(). 1923 * 1924 * @covers \completion_completion::mark_complete 1925 */ 1926 public function test_mark_complete() { 1927 global $DB; 1928 $this->resetAfterTest(true); 1929 1930 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 1931 1932 $student = $this->getDataGenerator()->create_user(); 1933 $teacher = $this->getDataGenerator()->create_user(); 1934 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 1935 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 1936 1937 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 1938 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 1939 1940 $this->setUser($teacher); 1941 1942 $cc = array( 1943 'course' => $course->id, 1944 'userid' => $student->id 1945 ); 1946 $ccompletion = new completion_completion($cc); 1947 1948 $completions = $DB->get_records('course_completions'); 1949 $this->assertEmpty($completions); 1950 1951 $completionid = $ccompletion->mark_complete(); 1952 $completions = $DB->get_records('course_completions'); 1953 $this->assertEquals(1, count($completions)); 1954 $this->assertEquals(reset($completions)->id, $completionid); 1955 1956 $ccompletion->id = 0; 1957 $completionid = $ccompletion->mark_complete(); 1958 $this->assertNull($completionid); 1959 $completions = $DB->get_records('course_completions'); 1960 $this->assertEquals(1, count($completions)); 1961 } 1962 1963 /** 1964 * Test for completion_criteria_completion::mark_complete(). 1965 * 1966 * @covers \completion_criteria_completion::mark_complete 1967 */ 1968 public function test_criteria_mark_complete() { 1969 global $DB; 1970 $this->resetAfterTest(true); 1971 1972 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 1973 1974 $student = $this->getDataGenerator()->create_user(); 1975 $teacher = $this->getDataGenerator()->create_user(); 1976 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 1977 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 1978 1979 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 1980 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 1981 1982 $this->setUser($teacher); 1983 1984 $record = [ 1985 'course' => $course->id, 1986 'criteriaid' => 1, 1987 'userid' => $student->id, 1988 'timecompleted' => time() 1989 ]; 1990 $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY); 1991 1992 $completions = $DB->get_records('course_completions'); 1993 $this->assertEmpty($completions); 1994 1995 $completionid = $completion->mark_complete($record['timecompleted']); 1996 $completions = $DB->get_records('course_completions'); 1997 $this->assertEquals(1, count($completions)); 1998 $this->assertEquals(reset($completions)->id, $completionid); 1999 } 2000 2001 /** 2002 * Test that data is cleaned when we reset a course completion data 2003 * 2004 * @covers ::delete_all_completion_data 2005 */ 2006 public function test_course_reset_completion() { 2007 global $DB; 2008 2009 $this->setup_data(); 2010 2011 $page = $this->getDataGenerator()->create_module('page', [ 2012 'course' => $this->course->id, 2013 'completion' => COMPLETION_ENABLED, 2014 'completionview' => COMPLETION_VIEW_REQUIRED, 2015 ]); 2016 $cm = cm_info::create(get_coursemodule_from_instance('page', $page->id)); 2017 $completion = new completion_info($this->course); 2018 $completion->set_module_viewed($cm, $this->user->id); 2019 // Sanity test. 2020 $this->assertTrue($DB->record_exists_select('course_modules_completion', 2021 'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)', 2022 ['course' => $this->course->id] 2023 )); 2024 $this->assertTrue($DB->record_exists_select('course_modules_viewed', 2025 'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)', 2026 ['course' => $this->course->id] 2027 )); 2028 // Deleting the prerequisite course should remove the completion criteria. 2029 $resetdata = new \stdClass(); 2030 $resetdata->id = $this->course->id; 2031 $resetdata->reset_completion = true; 2032 reset_course_userdata($resetdata); 2033 2034 $this->assertFalse($DB->record_exists_select('course_modules_completion', 2035 'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)', 2036 ['course' => $this->course->id] 2037 )); 2038 $this->assertFalse($DB->record_exists_select('course_modules_viewed', 2039 'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)', 2040 ['course' => $this->course->id] 2041 )); 2042 } 2043 } 2044 2045 class core_completionlib_fake_recordset implements Iterator { 2046 protected $closed; 2047 protected $values, $index; 2048 2049 public function __construct($values) { 2050 $this->values = $values; 2051 $this->index = 0; 2052 } 2053 2054 #[\ReturnTypeWillChange] 2055 public function current() { 2056 return $this->values[$this->index]; 2057 } 2058 2059 #[\ReturnTypeWillChange] 2060 public function key() { 2061 return $this->values[$this->index]; 2062 } 2063 2064 public function next(): void { 2065 $this->index++; 2066 } 2067 2068 public function rewind(): void { 2069 $this->index = 0; 2070 } 2071 2072 public function valid(): bool { 2073 return count($this->values) > $this->index; 2074 } 2075 2076 public function close() { 2077 $this->closed = true; 2078 } 2079 2080 public function was_closed() { 2081 return $this->closed; 2082 } 2083 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body