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