Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
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 } 1493 1494 /** 1495 * Test course completed event. 1496 * 1497 * @covers \core\event\course_completed 1498 */ 1499 public function test_course_completed_event() { 1500 global $USER; 1501 1502 $this->setup_data(); 1503 $this->setAdminUser(); 1504 1505 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 1506 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id)); 1507 1508 // Mark course as complete and get triggered event. 1509 $sink = $this->redirectEvents(); 1510 $ccompletion->mark_complete(); 1511 $events = $sink->get_events(); 1512 $event = reset($events); 1513 1514 $this->assertInstanceOf('\core\event\course_completed', $event); 1515 $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course); 1516 $this->assertEquals($this->course->id, $event->courseid); 1517 $this->assertEquals($USER->id, $event->userid); 1518 $this->assertEquals($this->user->id, $event->relateduserid); 1519 $this->assertEquals(context_course::instance($this->course->id), $event->get_context()); 1520 $this->assertInstanceOf('moodle_url', $event->get_url()); 1521 } 1522 1523 /** 1524 * Test course completed message. 1525 * 1526 * @covers \core\event\course_completed 1527 */ 1528 public function test_course_completed_message() { 1529 $this->setup_data(); 1530 $this->setAdminUser(); 1531 1532 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 1533 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id)); 1534 1535 // Mark course as complete and get the message. 1536 $sink = $this->redirectMessages(); 1537 $ccompletion->mark_complete(); 1538 $messages = $sink->get_messages(); 1539 $sink->close(); 1540 1541 $this->assertCount(1, $messages); 1542 $message = array_pop($messages); 1543 1544 $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom); 1545 $this->assertEquals($this->user->id, $message->useridto); 1546 $this->assertEquals('coursecompleted', $message->eventtype); 1547 $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject); 1548 $this->assertStringContainsString($this->course->fullname, $message->fullmessage); 1549 } 1550 1551 /** 1552 * Test course completed event. 1553 * 1554 * @covers \core\event\course_completion_updated 1555 */ 1556 public function test_course_completion_updated_event() { 1557 $this->setup_data(); 1558 $coursecontext = context_course::instance($this->course->id); 1559 $coursecompletionevent = \core\event\course_completion_updated::create( 1560 array( 1561 'courseid' => $this->course->id, 1562 'context' => $coursecontext 1563 ) 1564 ); 1565 1566 // Mark course as complete and get triggered event. 1567 $sink = $this->redirectEvents(); 1568 $coursecompletionevent->trigger(); 1569 $events = $sink->get_events(); 1570 $event = array_pop($events); 1571 $sink->close(); 1572 1573 $this->assertInstanceOf('\core\event\course_completion_updated', $event); 1574 $this->assertEquals($this->course->id, $event->courseid); 1575 $this->assertEquals($coursecontext, $event->get_context()); 1576 $this->assertInstanceOf('moodle_url', $event->get_url()); 1577 } 1578 1579 /** 1580 * @covers \completion_can_view_data 1581 */ 1582 public function test_completion_can_view_data() { 1583 $this->setup_data(); 1584 1585 $student = $this->getDataGenerator()->create_user(); 1586 $this->getDataGenerator()->enrol_user($student->id, $this->course->id); 1587 1588 $this->setUser($student); 1589 $this->assertTrue(completion_can_view_data($student->id, $this->course->id)); 1590 $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id)); 1591 } 1592 1593 /** 1594 * Data provider for test_get_grade_completion(). 1595 * 1596 * @return array[] 1597 */ 1598 public function get_grade_completion_provider() { 1599 return [ 1600 'Grade not required' => [false, false, null, null, null], 1601 'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE], 1602 'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE], 1603 'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS], 1604 'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL], 1605 ]; 1606 } 1607 1608 /** 1609 * Test for \completion_info::get_grade_completion(). 1610 * 1611 * @dataProvider get_grade_completion_provider 1612 * @param bool $completionusegrade Whether the test activity has grade completion requirement. 1613 * @param bool $hasgrade Whether to set grade for the user in this activity. 1614 * @param int|null $passinggrade Passing grade to set for the test activity. 1615 * @param string|null $expectedexception Expected exception. 1616 * @param int|null $expectedresult The expected completion status. 1617 * @covers ::get_grade_completion 1618 */ 1619 public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?int $passinggrade, 1620 ?string $expectedexception, ?int $expectedresult) { 1621 $this->setup_data(); 1622 1623 /** @var \mod_assign_generator $assigngenerator */ 1624 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); 1625 $assign = $assigngenerator->create_instance([ 1626 'course' => $this->course->id, 1627 'completion' => COMPLETION_ENABLED, 1628 'completionusegrade' => $completionusegrade, 1629 'gradepass' => $passinggrade, 1630 ]); 1631 1632 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id)); 1633 if ($completionusegrade && $hasgrade) { 1634 $assigninstance = new assign($cm->context, $cm, $this->course); 1635 $grade = $assigninstance->get_user_grade($this->user->id, true); 1636 $grade->grade = 75; 1637 $assigninstance->update_grade($grade); 1638 } 1639 1640 $completioninfo = new completion_info($this->course); 1641 if ($expectedexception) { 1642 $this->expectException($expectedexception); 1643 } 1644 $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user->id); 1645 $this->assertEquals($expectedresult, $gradecompletion); 1646 } 1647 1648 /** 1649 * Test the return value for cases when the activity module does not have associated grade_item. 1650 * 1651 * @covers ::get_grade_completion 1652 */ 1653 public function test_get_grade_completion_without_grade_item() { 1654 global $DB; 1655 1656 $this->setup_data(); 1657 1658 $assign = $this->getDataGenerator()->get_plugin_generator('mod_assign')->create_instance([ 1659 'course' => $this->course->id, 1660 'completion' => COMPLETION_ENABLED, 1661 'completionusegrade' => true, 1662 'gradepass' => 42, 1663 ]); 1664 1665 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id)); 1666 1667 $DB->delete_records('grade_items', [ 1668 'courseid' => $this->course->id, 1669 'itemtype' => 'mod', 1670 'itemmodule' => 'assign', 1671 'iteminstance' => $assign->id, 1672 ]); 1673 1674 // Without the grade_item, the activity is considered incomplete. 1675 $completioninfo = new completion_info($this->course); 1676 $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id)); 1677 1678 // Once the activity is graded, the grade_item is automatically created. 1679 $assigninstance = new assign($cm->context, $cm, $this->course); 1680 $grade = $assigninstance->get_user_grade($this->user->id, true); 1681 $grade->grade = 40; 1682 $assigninstance->update_grade($grade); 1683 1684 // The implicitly created grade_item does not have grade to pass defined so it is not distinguished. 1685 $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id)); 1686 } 1687 1688 /** 1689 * Test for aggregate_completions(). 1690 * 1691 * @covers \aggregate_completions 1692 */ 1693 public function test_aggregate_completions() { 1694 global $DB; 1695 $this->resetAfterTest(true); 1696 $time = time(); 1697 1698 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 1699 1700 for ($i = 0; $i < 4; $i++) { 1701 $students[] = $this->getDataGenerator()->create_user(); 1702 } 1703 1704 $teacher = $this->getDataGenerator()->create_user(); 1705 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 1706 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 1707 1708 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 1709 foreach ($students as $student) { 1710 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 1711 } 1712 1713 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), 1714 array('completion' => 1)); 1715 $cmdata = get_coursemodule_from_id('data', $data->cmid); 1716 1717 // Add activity completion criteria. 1718 $criteriadata = new stdClass(); 1719 $criteriadata->id = $course->id; 1720 $criteriadata->criteria_activity = array(); 1721 // Some activities. 1722 $criteriadata->criteria_activity[$cmdata->id] = 1; 1723 $class = 'completion_criteria_activity'; 1724 $criterion = new $class(); 1725 $criterion->update_config($criteriadata); 1726 1727 $this->setUser($teacher); 1728 1729 // Mark activity complete for both students. 1730 $cm = get_coursemodule_from_instance('data', $data->id); 1731 $completioncriteria = $DB->get_record('course_completion_criteria', []); 1732 foreach ($students as $student) { 1733 $cmcompletionrecords[] = (object)[ 1734 'coursemoduleid' => $cm->id, 1735 'userid' => $student->id, 1736 'completionstate' => 1, 1737 'viewed' => 0, 1738 'overrideby' => null, 1739 'timemodified' => 0, 1740 ]; 1741 1742 $usercompletions[] = (object)[ 1743 'criteriaid' => $completioncriteria->id, 1744 'userid' => $student->id, 1745 'timecompleted' => $time, 1746 ]; 1747 1748 $cc = array( 1749 'course' => $course->id, 1750 'userid' => $student->id 1751 ); 1752 $ccompletion = new completion_completion($cc); 1753 $completion[] = $ccompletion->mark_inprogress($time); 1754 } 1755 $DB->insert_records('course_modules_completion', $cmcompletionrecords); 1756 $DB->insert_records('course_completion_crit_compl', $usercompletions); 1757 1758 // MDL-33320: for instant completions we need aggregate to work in a single run. 1759 $DB->set_field('course_completions', 'reaggregate', $time - 2); 1760 1761 foreach ($students as $student) { 1762 $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]); 1763 $this->assertFalse($result); 1764 } 1765 1766 aggregate_completions($completion[0]); 1767 1768 $result1 = $DB->get_record('course_completions', ['userid' => $students[0]->id, 'reaggregate' => 0]); 1769 $result2 = $DB->get_record('course_completions', ['userid' => $students[1]->id, 'reaggregate' => 0]); 1770 $result3 = $DB->get_record('course_completions', ['userid' => $students[2]->id, 'reaggregate' => 0]); 1771 1772 $this->assertIsObject($result1); 1773 $this->assertFalse($result2); 1774 $this->assertFalse($result3); 1775 1776 aggregate_completions(0); 1777 1778 foreach ($students as $student) { 1779 $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]); 1780 $this->assertIsObject($result); 1781 } 1782 } 1783 1784 /** 1785 * Test for completion_completion::_save(). 1786 * 1787 * @covers \completion_completion::_save 1788 */ 1789 public function test_save() { 1790 global $DB; 1791 $this->resetAfterTest(true); 1792 1793 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 1794 1795 $student = $this->getDataGenerator()->create_user(); 1796 $teacher = $this->getDataGenerator()->create_user(); 1797 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 1798 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 1799 1800 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 1801 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 1802 1803 $this->setUser($teacher); 1804 1805 $cc = array( 1806 'course' => $course->id, 1807 'userid' => $student->id 1808 ); 1809 $ccompletion = new completion_completion($cc); 1810 1811 $completions = $DB->get_records('course_completions'); 1812 $this->assertEmpty($completions); 1813 1814 // We're testing a private method, so we need to setup reflector magic. 1815 $method = new ReflectionMethod($ccompletion, '_save'); 1816 $method->setAccessible(true); // Allow accessing of private method. 1817 $completionid = $method->invoke($ccompletion); 1818 $completions = $DB->get_records('course_completions'); 1819 $this->assertEquals(count($completions), 1); 1820 $this->assertEquals(reset($completions)->id, $completionid); 1821 1822 $ccompletion->id = 0; 1823 $method = new ReflectionMethod($ccompletion, '_save'); 1824 $method->setAccessible(true); // Allow accessing of private method. 1825 $completionid = $method->invoke($ccompletion); 1826 $this->assertDebuggingCalled('Can not update data object, no id!'); 1827 $this->assertNull($completionid); 1828 } 1829 1830 /** 1831 * Test for completion_completion::mark_enrolled(). 1832 * 1833 * @covers \completion_completion::mark_enrolled 1834 */ 1835 public function test_mark_enrolled() { 1836 global $DB; 1837 $this->resetAfterTest(true); 1838 1839 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 1840 1841 $student = $this->getDataGenerator()->create_user(); 1842 $teacher = $this->getDataGenerator()->create_user(); 1843 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 1844 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 1845 1846 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 1847 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 1848 1849 $this->setUser($teacher); 1850 1851 $cc = array( 1852 'course' => $course->id, 1853 'userid' => $student->id 1854 ); 1855 $ccompletion = new completion_completion($cc); 1856 1857 $completions = $DB->get_records('course_completions'); 1858 $this->assertEmpty($completions); 1859 1860 $completionid = $ccompletion->mark_enrolled(); 1861 $completions = $DB->get_records('course_completions'); 1862 $this->assertEquals(count($completions), 1); 1863 $this->assertEquals(reset($completions)->id, $completionid); 1864 1865 $ccompletion->id = 0; 1866 $completionid = $ccompletion->mark_enrolled(); 1867 $this->assertDebuggingCalled('Can not update data object, no id!'); 1868 $this->assertNull($completionid); 1869 $completions = $DB->get_records('course_completions'); 1870 $this->assertEquals(1, count($completions)); 1871 } 1872 1873 /** 1874 * Test for completion_completion::mark_inprogress(). 1875 * 1876 * @covers \completion_completion::mark_inprogress 1877 */ 1878 public function test_mark_inprogress() { 1879 global $DB; 1880 $this->resetAfterTest(true); 1881 1882 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 1883 1884 $student = $this->getDataGenerator()->create_user(); 1885 $teacher = $this->getDataGenerator()->create_user(); 1886 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 1887 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 1888 1889 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 1890 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 1891 1892 $this->setUser($teacher); 1893 1894 $cc = array( 1895 'course' => $course->id, 1896 'userid' => $student->id 1897 ); 1898 $ccompletion = new completion_completion($cc); 1899 1900 $completions = $DB->get_records('course_completions'); 1901 $this->assertEmpty($completions); 1902 1903 $completionid = $ccompletion->mark_inprogress(); 1904 $completions = $DB->get_records('course_completions'); 1905 $this->assertEquals(1, count($completions)); 1906 $this->assertEquals(reset($completions)->id, $completionid); 1907 1908 $ccompletion->id = 0; 1909 $completionid = $ccompletion->mark_inprogress(); 1910 $this->assertDebuggingCalled('Can not update data object, no id!'); 1911 $this->assertNull($completionid); 1912 $completions = $DB->get_records('course_completions'); 1913 $this->assertEquals(1, count($completions)); 1914 } 1915 1916 /** 1917 * Test for completion_completion::mark_complete(). 1918 * 1919 * @covers \completion_completion::mark_complete 1920 */ 1921 public function test_mark_complete() { 1922 global $DB; 1923 $this->resetAfterTest(true); 1924 1925 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 1926 1927 $student = $this->getDataGenerator()->create_user(); 1928 $teacher = $this->getDataGenerator()->create_user(); 1929 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 1930 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 1931 1932 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 1933 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 1934 1935 $this->setUser($teacher); 1936 1937 $cc = array( 1938 'course' => $course->id, 1939 'userid' => $student->id 1940 ); 1941 $ccompletion = new completion_completion($cc); 1942 1943 $completions = $DB->get_records('course_completions'); 1944 $this->assertEmpty($completions); 1945 1946 $completionid = $ccompletion->mark_complete(); 1947 $completions = $DB->get_records('course_completions'); 1948 $this->assertEquals(1, count($completions)); 1949 $this->assertEquals(reset($completions)->id, $completionid); 1950 1951 $ccompletion->id = 0; 1952 $completionid = $ccompletion->mark_complete(); 1953 $this->assertNull($completionid); 1954 $completions = $DB->get_records('course_completions'); 1955 $this->assertEquals(1, count($completions)); 1956 } 1957 1958 /** 1959 * Test for completion_criteria_completion::mark_complete(). 1960 * 1961 * @covers \completion_criteria_completion::mark_complete 1962 */ 1963 public function test_criteria_mark_complete() { 1964 global $DB; 1965 $this->resetAfterTest(true); 1966 1967 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 1968 1969 $student = $this->getDataGenerator()->create_user(); 1970 $teacher = $this->getDataGenerator()->create_user(); 1971 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 1972 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 1973 1974 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 1975 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 1976 1977 $this->setUser($teacher); 1978 1979 $record = [ 1980 'course' => $course->id, 1981 'criteriaid' => 1, 1982 'userid' => $student->id, 1983 'timecompleted' => time() 1984 ]; 1985 $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY); 1986 1987 $completions = $DB->get_records('course_completions'); 1988 $this->assertEmpty($completions); 1989 1990 $completionid = $completion->mark_complete($record['timecompleted']); 1991 $completions = $DB->get_records('course_completions'); 1992 $this->assertEquals(1, count($completions)); 1993 $this->assertEquals(reset($completions)->id, $completionid); 1994 } 1995 1996 /** 1997 * Test that data is cleaned when we reset a course completion data 1998 * 1999 * @covers ::delete_all_completion_data 2000 */ 2001 public function test_course_reset_completion() { 2002 global $DB; 2003 2004 $this->setup_data(); 2005 2006 $page = $this->getDataGenerator()->create_module('page', [ 2007 'course' => $this->course->id, 2008 'completion' => COMPLETION_ENABLED, 2009 'completionview' => COMPLETION_VIEW_REQUIRED, 2010 ]); 2011 $cm = cm_info::create(get_coursemodule_from_instance('page', $page->id)); 2012 $completion = new completion_info($this->course); 2013 $completion->set_module_viewed($cm, $this->user->id); 2014 // Sanity test. 2015 $this->assertTrue($DB->record_exists_select('course_modules_completion', 2016 'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)', 2017 ['course' => $this->course->id] 2018 )); 2019 $this->assertTrue($DB->record_exists_select('course_modules_viewed', 2020 'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)', 2021 ['course' => $this->course->id] 2022 )); 2023 // Deleting the prerequisite course should remove the completion criteria. 2024 $resetdata = new \stdClass(); 2025 $resetdata->id = $this->course->id; 2026 $resetdata->reset_completion = true; 2027 reset_course_userdata($resetdata); 2028 2029 $this->assertFalse($DB->record_exists_select('course_modules_completion', 2030 'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)', 2031 ['course' => $this->course->id] 2032 )); 2033 $this->assertFalse($DB->record_exists_select('course_modules_viewed', 2034 'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)', 2035 ['course' => $this->course->id] 2036 )); 2037 } 2038 } 2039 2040 class core_completionlib_fake_recordset implements Iterator { 2041 protected $closed; 2042 protected $values, $index; 2043 2044 public function __construct($values) { 2045 $this->values = $values; 2046 $this->index = 0; 2047 } 2048 2049 #[\ReturnTypeWillChange] 2050 public function current() { 2051 return $this->values[$this->index]; 2052 } 2053 2054 #[\ReturnTypeWillChange] 2055 public function key() { 2056 return $this->values[$this->index]; 2057 } 2058 2059 public function next(): void { 2060 $this->index++; 2061 } 2062 2063 public function rewind(): void { 2064 $this->index = 0; 2065 } 2066 2067 public function valid(): bool { 2068 return count($this->values) > $this->index; 2069 } 2070 2071 public function close() { 2072 $this->closed = true; 2073 } 2074 2075 public function was_closed() { 2076 return $this->closed; 2077 } 2078 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body