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