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 27defined('MOODLE_INTERNAL') || die(); 28 29global $CFG; 30require_once($CFG->libdir.'/completionlib.php'); 31 32class core_completionlib_testcase 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_object($expected) and is_object($actual)) { 83 if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) { 84 if ($expected->timemodified + 1 == $actual->timemodified) { 85 $expected = clone($expected); 86 $expected->timemodified = $actual->timemodified; 87 } 88 } 89 } 90 parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase); 91 } 92 93 public function test_is_enabled() { 94 global $CFG; 95 $this->mock_setup(); 96 97 // Config alone. 98 $CFG->enablecompletion = COMPLETION_DISABLED; 99 $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site()); 100 $CFG->enablecompletion = COMPLETION_ENABLED; 101 $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site()); 102 103 // Course. 104 $course = (object)array('id' =>13); 105 $c = new completion_info($course); 106 $course->enablecompletion = COMPLETION_DISABLED; 107 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled()); 108 $course->enablecompletion = COMPLETION_ENABLED; 109 $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled()); 110 $CFG->enablecompletion = COMPLETION_DISABLED; 111 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled()); 112 113 // Course and CM. 114 $cm = new stdClass(); 115 $cm->completion = COMPLETION_TRACKING_MANUAL; 116 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm)); 117 $CFG->enablecompletion = COMPLETION_ENABLED; 118 $course->enablecompletion = COMPLETION_DISABLED; 119 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm)); 120 $course->enablecompletion = COMPLETION_ENABLED; 121 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm)); 122 $cm->completion = COMPLETION_TRACKING_NONE; 123 $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm)); 124 $cm->completion = COMPLETION_TRACKING_AUTOMATIC; 125 $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm)); 126 } 127 128 public function test_update_state() { 129 $this->mock_setup(); 130 131 $mockbuilder = $this->getMockBuilder('completion_info'); 132 $mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data', 133 'user_can_override_completion')); 134 $mockbuilder->setConstructorArgs(array((object)array('id' => 42))); 135 $c = $mockbuilder->getMock(); 136 $cm = (object)array('id'=>13, 'course'=>42); 137 138 // Not enabled, should do nothing. 139 $c->expects($this->at(0)) 140 ->method('is_enabled') 141 ->with($cm) 142 ->will($this->returnValue(false)); 143 $c->update_state($cm); 144 145 // Enabled, but current state is same as possible result, do nothing. 146 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null); 147 $c->expects($this->at(0)) 148 ->method('is_enabled') 149 ->with($cm) 150 ->will($this->returnValue(true)); 151 $c->expects($this->at(1)) 152 ->method('get_data') 153 ->with($cm, false, 0) 154 ->will($this->returnValue($current)); 155 $c->update_state($cm, COMPLETION_COMPLETE); 156 157 // Enabled, but current state is a specific one and new state is just 158 // complete, so do nothing. 159 $current->completionstate = COMPLETION_COMPLETE_PASS; 160 $c->expects($this->at(0)) 161 ->method('is_enabled') 162 ->with($cm) 163 ->will($this->returnValue(true)); 164 $c->expects($this->at(1)) 165 ->method('get_data') 166 ->with($cm, false, 0) 167 ->will($this->returnValue($current)); 168 $c->update_state($cm, COMPLETION_COMPLETE); 169 170 // Manual, change state (no change). 171 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_MANUAL); 172 $current->completionstate=COMPLETION_COMPLETE; 173 $c->expects($this->at(0)) 174 ->method('is_enabled') 175 ->with($cm) 176 ->will($this->returnValue(true)); 177 $c->expects($this->at(1)) 178 ->method('get_data') 179 ->with($cm, false, 0) 180 ->will($this->returnValue($current)); 181 $c->update_state($cm, COMPLETION_COMPLETE); 182 183 // Manual, change state (change). 184 $c->expects($this->at(0)) 185 ->method('is_enabled') 186 ->with($cm) 187 ->will($this->returnValue(true)); 188 $c->expects($this->at(1)) 189 ->method('get_data') 190 ->with($cm, false, 0) 191 ->will($this->returnValue($current)); 192 $changed = clone($current); 193 $changed->timemodified = time(); 194 $changed->completionstate = COMPLETION_INCOMPLETE; 195 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed); 196 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual'); 197 $c->expects($this->at(2)) 198 ->method('internal_set_data') 199 ->with($cm, $comparewith); 200 $c->update_state($cm, COMPLETION_INCOMPLETE); 201 202 // Auto, change state. 203 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC); 204 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null); 205 $c->expects($this->at(0)) 206 ->method('is_enabled') 207 ->with($cm) 208 ->will($this->returnValue(true)); 209 $c->expects($this->at(1)) 210 ->method('get_data') 211 ->with($cm, false, 0) 212 ->will($this->returnValue($current)); 213 $c->expects($this->at(2)) 214 ->method('internal_get_state') 215 ->will($this->returnValue(COMPLETION_COMPLETE_PASS)); 216 $changed = clone($current); 217 $changed->timemodified = time(); 218 $changed->completionstate = COMPLETION_COMPLETE_PASS; 219 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed); 220 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual'); 221 $c->expects($this->at(3)) 222 ->method('internal_set_data') 223 ->with($cm, $comparewith); 224 $c->update_state($cm, COMPLETION_COMPLETE_PASS); 225 226 // Manual tracking, change state by overriding it manually. 227 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL); 228 $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null); 229 $c->expects($this->at(0)) 230 ->method('is_enabled') 231 ->with($cm) 232 ->will($this->returnValue(true)); 233 $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses. 234 ->method('user_can_override_completion') 235 ->will($this->returnValue(true)); 236 $c->expects($this->at(2)) 237 ->method('get_data') 238 ->with($cm, false, 100) 239 ->will($this->returnValue($current)); 240 $changed = clone($current); 241 $changed->timemodified = time(); 242 $changed->completionstate = COMPLETION_COMPLETE; 243 $changed->overrideby = 314159; 244 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed); 245 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual'); 246 $c->expects($this->at(3)) 247 ->method('internal_set_data') 248 ->with($cm, $comparewith); 249 $c->update_state($cm, COMPLETION_COMPLETE, 100, true); 250 // And confirm that the status can be changed back to incomplete without an override. 251 $c->update_state($cm, COMPLETION_INCOMPLETE, 100); 252 $c->expects($this->at(0)) 253 ->method('get_data') 254 ->with($cm, false, 100) 255 ->will($this->returnValue($current)); 256 $c->get_data($cm, false, 100); 257 258 // Auto, change state via override, incomplete to complete. 259 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC); 260 $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null); 261 $c->expects($this->at(0)) 262 ->method('is_enabled') 263 ->with($cm) 264 ->will($this->returnValue(true)); 265 $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses. 266 ->method('user_can_override_completion') 267 ->will($this->returnValue(true)); 268 $c->expects($this->at(2)) 269 ->method('get_data') 270 ->with($cm, false, 100) 271 ->will($this->returnValue($current)); 272 $changed = clone($current); 273 $changed->timemodified = time(); 274 $changed->completionstate = COMPLETION_COMPLETE; 275 $changed->overrideby = 314159; 276 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed); 277 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual'); 278 $c->expects($this->at(3)) 279 ->method('internal_set_data') 280 ->with($cm, $comparewith); 281 $c->update_state($cm, COMPLETION_COMPLETE, 100, true); 282 $c->expects($this->at(0)) 283 ->method('get_data') 284 ->with($cm, false, 100) 285 ->will($this->returnValue($changed)); 286 $c->get_data($cm, false, 100); 287 288 // Now confirm that the status cannot be changed back to incomplete without an override. 289 // I.e. test that automatic completion won't trigger a change back to COMPLETION_INCOMPLETE when overridden. 290 $c->update_state($cm, COMPLETION_INCOMPLETE, 100); 291 $c->expects($this->at(0)) 292 ->method('get_data') 293 ->with($cm, false, 100) 294 ->will($this->returnValue($changed)); 295 $c->get_data($cm, false, 100); 296 297 // Now confirm the status can be changed back from complete to incomplete using an override. 298 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC); 299 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2); 300 $c->expects($this->at(0)) 301 ->method('is_enabled') 302 ->with($cm) 303 ->will($this->returnValue(true)); 304 $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses. 305 ->method('user_can_override_completion') 306 ->will($this->returnValue(true)); 307 $c->expects($this->at(2)) 308 ->method('get_data') 309 ->with($cm, false, 100) 310 ->will($this->returnValue($current)); 311 $changed = clone($current); 312 $changed->timemodified = time(); 313 $changed->completionstate = COMPLETION_INCOMPLETE; 314 $changed->overrideby = 314159; 315 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed); 316 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual'); 317 $c->expects($this->at(3)) 318 ->method('internal_set_data') 319 ->with($cm, $comparewith); 320 $c->update_state($cm, COMPLETION_INCOMPLETE, 100, true); 321 $c->expects($this->at(0)) 322 ->method('get_data') 323 ->with($cm, false, 100) 324 ->will($this->returnValue($changed)); 325 $c->get_data($cm, false, 100); 326 } 327 328 public function test_internal_get_state() { 329 global $DB; 330 $this->mock_setup(); 331 332 $mockbuilder = $this->getMockBuilder('completion_info'); 333 $mockbuilder->setMethods(array('internal_get_grade_state')); 334 $mockbuilder->setConstructorArgs(array((object)array('id' => 42))); 335 $c = $mockbuilder->getMock(); 336 337 $cm = (object)array('id'=>13, 'course'=>42, 'completiongradeitemnumber'=>null); 338 339 // If view is required, but they haven't viewed it yet. 340 $cm->completionview = COMPLETION_VIEW_REQUIRED; 341 $current = (object)array('viewed'=>COMPLETION_NOT_VIEWED); 342 $this->assertEquals(COMPLETION_INCOMPLETE, $c->internal_get_state($cm, 123, $current)); 343 344 // OK set view not required. 345 $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED; 346 347 // Test not getting module name. 348 $cm->modname='label'; 349 $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current)); 350 351 // Test getting module name. 352 $cm->module = 13; 353 unset($cm->modname); 354 /** @var $DB PHPUnit_Framework_MockObject_MockObject */ 355 $DB->expects($this->once()) 356 ->method('get_field') 357 ->with('modules', 'name', array('id'=>13)) 358 ->will($this->returnValue('lable')); 359 $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current)); 360 361 // Note: This function is not fully tested (including kind of the main part) because: 362 // * the grade_item/grade_grade calls are static and can't be mocked, 363 // * the plugin_supports call is static and can't be mocked. 364 } 365 366 public function test_set_module_viewed() { 367 $this->mock_setup(); 368 369 $mockbuilder = $this->getMockBuilder('completion_info'); 370 $mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state')); 371 $mockbuilder->setConstructorArgs(array((object)array('id' => 42))); 372 $c = $mockbuilder->getMock(); 373 $cm = (object)array('id'=>13, 'course'=>42); 374 375 // Not tracking completion, should do nothing. 376 $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED; 377 $c->set_module_viewed($cm); 378 379 // Tracking completion but completion is disabled, should do nothing. 380 $cm->completionview = COMPLETION_VIEW_REQUIRED; 381 $c->expects($this->at(0)) 382 ->method('is_enabled') 383 ->with($cm) 384 ->will($this->returnValue(false)); 385 $c->set_module_viewed($cm); 386 387 // Now it's enabled, we expect it to get data. If data already has 388 // viewed, still do nothing. 389 $c->expects($this->at(0)) 390 ->method('is_enabled') 391 ->with($cm) 392 ->will($this->returnValue(true)); 393 $c->expects($this->at(1)) 394 ->method('get_data') 395 ->with($cm, 0) 396 ->will($this->returnValue((object)array('viewed'=>COMPLETION_VIEWED))); 397 $c->set_module_viewed($cm); 398 399 // OK finally one that hasn't been viewed, now it should set it viewed 400 // and update state. 401 $c->expects($this->at(0)) 402 ->method('is_enabled') 403 ->with($cm) 404 ->will($this->returnValue(true)); 405 $c->expects($this->at(1)) 406 ->method('get_data') 407 ->with($cm, false, 1337) 408 ->will($this->returnValue((object)array('viewed'=>COMPLETION_NOT_VIEWED))); 409 $c->expects($this->at(2)) 410 ->method('internal_set_data') 411 ->with($cm, (object)array('viewed'=>COMPLETION_VIEWED)); 412 $c->expects($this->at(3)) 413 ->method('update_state') 414 ->with($cm, COMPLETION_COMPLETE, 1337); 415 $c->set_module_viewed($cm, 1337); 416 } 417 418 public function test_count_user_data() { 419 global $DB; 420 $this->mock_setup(); 421 422 $course = (object)array('id'=>13); 423 $cm = (object)array('id'=>42); 424 425 /** @var $DB PHPUnit_Framework_MockObject_MockObject */ 426 $DB->expects($this->at(0)) 427 ->method('get_field_sql') 428 ->will($this->returnValue(666)); 429 430 $c = new completion_info($course); 431 $this->assertEquals(666, $c->count_user_data($cm)); 432 } 433 434 public function test_delete_all_state() { 435 global $DB; 436 $this->mock_setup(); 437 438 $course = (object)array('id'=>13); 439 $cm = (object)array('id'=>42, 'course'=>13); 440 $c = new completion_info($course); 441 442 // Check it works ok without data in session. 443 /** @var $DB PHPUnit_Framework_MockObject_MockObject */ 444 $DB->expects($this->at(0)) 445 ->method('delete_records') 446 ->with('course_modules_completion', array('coursemoduleid'=>42)) 447 ->will($this->returnValue(true)); 448 $c->delete_all_state($cm); 449 } 450 451 public function test_reset_all_state() { 452 global $DB; 453 $this->mock_setup(); 454 455 $mockbuilder = $this->getMockBuilder('completion_info'); 456 $mockbuilder->setMethods(array('delete_all_state', 'get_tracked_users', 'update_state')); 457 $mockbuilder->setConstructorArgs(array((object)array('id' => 42))); 458 $c = $mockbuilder->getMock(); 459 460 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC); 461 462 /** @var $DB PHPUnit_Framework_MockObject_MockObject */ 463 $DB->expects($this->at(0)) 464 ->method('get_recordset') 465 ->will($this->returnValue( 466 new core_completionlib_fake_recordset(array((object)array('id'=>1, 'userid'=>100), (object)array('id'=>2, 'userid'=>101))))); 467 468 $c->expects($this->at(0)) 469 ->method('delete_all_state') 470 ->with($cm); 471 472 $c->expects($this->at(1)) 473 ->method('get_tracked_users') 474 ->will($this->returnValue(array( 475 (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'), 476 (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy')))); 477 478 $c->expects($this->at(2)) 479 ->method('update_state') 480 ->with($cm, COMPLETION_UNKNOWN, 100); 481 $c->expects($this->at(3)) 482 ->method('update_state') 483 ->with($cm, COMPLETION_UNKNOWN, 101); 484 $c->expects($this->at(4)) 485 ->method('update_state') 486 ->with($cm, COMPLETION_UNKNOWN, 201); 487 488 $c->reset_all_state($cm); 489 } 490 491 /** 492 * Data provider for test_get_data(). 493 * 494 * @return array[] 495 */ 496 public function get_data_provider() { 497 return [ 498 'No completion record' => [ 499 false, true, false, COMPLETION_INCOMPLETE 500 ], 501 'Not completed' => [ 502 false, true, true, COMPLETION_INCOMPLETE 503 ], 504 'Completed' => [ 505 false, true, true, COMPLETION_COMPLETE 506 ], 507 'Whole course, complete' => [ 508 true, true, true, COMPLETION_COMPLETE 509 ], 510 'Get data for another user, result should be not cached' => [ 511 false, false, true, COMPLETION_INCOMPLETE 512 ], 513 'Get data for another user, including whole course, result should be not cached' => [ 514 true, false, true, COMPLETION_INCOMPLETE 515 ], 516 ]; 517 } 518 519 /** 520 * Tests for completion_info::get_data(). 521 * 522 * @dataProvider get_data_provider 523 * @param bool $wholecourse Whole course parameter for get_data(). 524 * @param bool $sameuser Whether the user calling get_data() is the user itself. 525 * @param bool $hasrecord Whether to create a course_modules_completion record. 526 * @param int $completion The completion state expected. 527 */ 528 public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion) { 529 global $DB; 530 531 $this->setup_data(); 532 $user = $this->user; 533 534 /** @var \mod_choice_generator $choicegenerator */ 535 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice'); 536 $choice = $choicegenerator->create_instance([ 537 'course' => $this->course->id, 538 'completion' => true, 539 'completionview' => true, 540 ]); 541 542 $cm = get_coursemodule_from_instance('choice', $choice->id); 543 544 // Let's manually create a course completion record instead of going thru the hoops to complete an activity. 545 if ($hasrecord) { 546 $cmcompletionrecord = (object)[ 547 'coursemoduleid' => $cm->id, 548 'userid' => $user->id, 549 'completionstate' => $completion, 550 'viewed' => 0, 551 'overrideby' => null, 552 'timemodified' => 0, 553 ]; 554 $DB->insert_record('course_modules_completion', $cmcompletionrecord); 555 } 556 557 // Whether we expect for the returned completion data to be stored in the cache. 558 $iscached = true; 559 560 if (!$sameuser) { 561 $iscached = false; 562 $this->setAdminUser(); 563 } else { 564 $this->setUser($user); 565 } 566 567 // Mock other completion data. 568 $completioninfo = new completion_info($this->course); 569 570 $result = $completioninfo->get_data($cm, $wholecourse, $user->id); 571 // Course module ID of the returned completion data must match this activity's course module ID. 572 $this->assertEquals($cm->id, $result->coursemoduleid); 573 // User ID of the returned completion data must match the user's ID. 574 $this->assertEquals($user->id, $result->userid); 575 // The completion state of the returned completion data must match the expected completion state. 576 $this->assertEquals($completion, $result->completionstate); 577 578 // If the user has no completion record, then the default record should be returned. 579 if (!$hasrecord) { 580 $this->assertEquals(0, $result->id); 581 } 582 583 // Check caching. 584 $key = "{$user->id}_{$this->course->id}"; 585 $cache = cache::make('core', 'completion'); 586 if ($iscached) { 587 // If we expect this to be cached, then fetching the result must match the cached data. 588 $this->assertEquals($result, (object)$cache->get($key)[$cm->id]); 589 590 // Check cached data for other course modules in the course. 591 // The sample module created in setup_data() should suffice to confirm this. 592 $othercm = get_coursemodule_from_instance('forum', $this->module1->id); 593 if ($wholecourse) { 594 $this->assertArrayHasKey($othercm->id, $cache->get($key)); 595 } else { 596 $this->assertArrayNotHasKey($othercm->id, $cache->get($key)); 597 } 598 } else { 599 // Otherwise, this should not be cached. 600 $this->assertFalse($cache->get($key)); 601 } 602 } 603 604 public function test_internal_set_data() { 605 global $DB; 606 $this->setup_data(); 607 608 $this->setUser($this->user); 609 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 610 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto); 611 $cm = get_coursemodule_from_instance('forum', $forum->id); 612 $c = new completion_info($this->course); 613 614 // 1) Test with new data. 615 $data = new stdClass(); 616 $data->id = 0; 617 $data->userid = $this->user->id; 618 $data->coursemoduleid = $cm->id; 619 $data->completionstate = COMPLETION_COMPLETE; 620 $data->timemodified = time(); 621 $data->viewed = COMPLETION_NOT_VIEWED; 622 $data->overrideby = null; 623 624 $c->internal_set_data($cm, $data); 625 $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id)); 626 $this->assertEquals($d1, $data->id); 627 $cache = cache::make('core', 'completion'); 628 // Cache was not set for another user. 629 $this->assertEquals(array('cacherev' => $this->course->cacherev, $cm->id => $data), 630 $cache->get($data->userid . '_' . $cm->course)); 631 632 // 2) Test with existing data and for different user. 633 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto); 634 $cm2 = get_coursemodule_from_instance('forum', $forum2->id); 635 $newuser = $this->getDataGenerator()->create_user(); 636 637 $d2 = new stdClass(); 638 $d2->id = 7; 639 $d2->userid = $newuser->id; 640 $d2->coursemoduleid = $cm2->id; 641 $d2->completionstate = COMPLETION_COMPLETE; 642 $d2->timemodified = time(); 643 $d2->viewed = COMPLETION_NOT_VIEWED; 644 $d2->overrideby = null; 645 $c->internal_set_data($cm2, $d2); 646 // Cache for current user returns the data. 647 $cachevalue = $cache->get($data->userid . '_' . $cm->course); 648 $this->assertEquals($data, $cachevalue[$cm->id]); 649 // Cache for another user is not filled. 650 $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course)); 651 652 // 3) Test where it THINKS the data is new (from cache) but actually 653 // in the database it has been set since. 654 // 1) Test with new data. 655 $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto); 656 $cm3 = get_coursemodule_from_instance('forum', $forum3->id); 657 $newuser2 = $this->getDataGenerator()->create_user(); 658 $d3 = new stdClass(); 659 $d3->id = 13; 660 $d3->userid = $newuser2->id; 661 $d3->coursemoduleid = $cm3->id; 662 $d3->completionstate = COMPLETION_COMPLETE; 663 $d3->timemodified = time(); 664 $d3->viewed = COMPLETION_NOT_VIEWED; 665 $d3->overrideby = null; 666 $DB->insert_record('course_modules_completion', $d3); 667 $c->internal_set_data($cm, $data); 668 } 669 670 public function test_get_progress_all() { 671 global $DB; 672 $this->mock_setup(); 673 674 $mockbuilder = $this->getMockBuilder('completion_info'); 675 $mockbuilder->setMethods(array('get_tracked_users')); 676 $mockbuilder->setConstructorArgs(array((object)array('id' => 42))); 677 $c = $mockbuilder->getMock(); 678 679 // 1) Basic usage. 680 $c->expects($this->at(0)) 681 ->method('get_tracked_users') 682 ->with(false, array(), 0, '', '', '', null) 683 ->will($this->returnValue(array( 684 (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'), 685 (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy')))); 686 $DB->expects($this->at(0)) 687 ->method('get_in_or_equal') 688 ->with(array(100, 201)) 689 ->will($this->returnValue(array(' IN (100, 201)', array()))); 690 $progress1 = (object)array('userid'=>100, 'coursemoduleid'=>13); 691 $progress2 = (object)array('userid'=>201, 'coursemoduleid'=>14); 692 $DB->expects($this->at(1)) 693 ->method('get_recordset_sql') 694 ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2)))); 695 696 $this->assertEquals(array( 697 100 => (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh', 698 'progress'=>array(13=>$progress1)), 699 201 => (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy', 700 'progress'=>array(14=>$progress2)), 701 ), $c->get_progress_all(false)); 702 703 // 2) With more than 1, 000 results. 704 $tracked = array(); 705 $ids = array(); 706 $progress = array(); 707 for ($i = 100; $i<2000; $i++) { 708 $tracked[] = (object)array('id'=>$i, 'firstname'=>'frog', 'lastname'=>$i); 709 $ids[] = $i; 710 $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>13); 711 $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>14); 712 } 713 $c->expects($this->at(0)) 714 ->method('get_tracked_users') 715 ->with(true, 3, 0, '', '', '', null) 716 ->will($this->returnValue($tracked)); 717 $DB->expects($this->at(0)) 718 ->method('get_in_or_equal') 719 ->with(array_slice($ids, 0, 1000)) 720 ->will($this->returnValue(array(' IN whatever', array()))); 721 $DB->expects($this->at(1)) 722 ->method('get_recordset_sql') 723 ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 0, 1000)))); 724 725 $DB->expects($this->at(2)) 726 ->method('get_in_or_equal') 727 ->with(array_slice($ids, 1000)) 728 ->will($this->returnValue(array(' IN whatever2', array()))); 729 $DB->expects($this->at(3)) 730 ->method('get_recordset_sql') 731 ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 1000)))); 732 733 $result = $c->get_progress_all(true, 3); 734 $resultok = true; 735 $resultok = $resultok && ($ids == array_keys($result)); 736 737 foreach ($result as $userid => $data) { 738 $resultok = $resultok && $data->firstname == 'frog'; 739 $resultok = $resultok && $data->lastname == $userid; 740 $resultok = $resultok && $data->id == $userid; 741 $cms = $data->progress; 742 $resultok = $resultok && (array(13, 14) == array_keys($cms)); 743 $resultok = $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>13) == $cms[13]); 744 $resultok = $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>14) == $cms[14]); 745 } 746 $this->assertTrue($resultok); 747 } 748 749 public function test_inform_grade_changed() { 750 $this->mock_setup(); 751 752 $mockbuilder = $this->getMockBuilder('completion_info'); 753 $mockbuilder->setMethods(array('is_enabled', 'update_state')); 754 $mockbuilder->setConstructorArgs(array((object)array('id' => 42))); 755 $c = $mockbuilder->getMock(); 756 757 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>null); 758 $item = (object)array('itemnumber'=>3, 'gradepass'=>1, 'hidden'=>0); 759 $grade = (object)array('userid'=>31337, 'finalgrade'=>0, 'rawgrade'=>0); 760 761 // Not enabled (should do nothing). 762 $c->expects($this->at(0)) 763 ->method('is_enabled') 764 ->with($cm) 765 ->will($this->returnValue(false)); 766 $c->inform_grade_changed($cm, $item, $grade, false); 767 768 // Enabled but still no grade completion required, should still do nothing. 769 $c->expects($this->at(0)) 770 ->method('is_enabled') 771 ->with($cm) 772 ->will($this->returnValue(true)); 773 $c->inform_grade_changed($cm, $item, $grade, false); 774 775 // Enabled and completion required but item number is wrong, does nothing. 776 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>7); 777 $c->expects($this->at(0)) 778 ->method('is_enabled') 779 ->with($cm) 780 ->will($this->returnValue(true)); 781 $c->inform_grade_changed($cm, $item, $grade, false); 782 783 // Enabled and completion required and item number right. It is supposed 784 // to call update_state with the new potential state being obtained from 785 // internal_get_grade_state. 786 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3); 787 $grade = (object)array('userid'=>31337, 'finalgrade'=>1, 'rawgrade'=>0); 788 $c->expects($this->at(0)) 789 ->method('is_enabled') 790 ->with($cm) 791 ->will($this->returnValue(true)); 792 $c->expects($this->at(1)) 793 ->method('update_state') 794 ->with($cm, COMPLETION_COMPLETE_PASS, 31337) 795 ->will($this->returnValue(true)); 796 $c->inform_grade_changed($cm, $item, $grade, false); 797 798 // Same as above but marked deleted. It is supposed to call update_state 799 // with new potential state being COMPLETION_INCOMPLETE. 800 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3); 801 $grade = (object)array('userid'=>31337, 'finalgrade'=>1, 'rawgrade'=>0); 802 $c->expects($this->at(0)) 803 ->method('is_enabled') 804 ->with($cm) 805 ->will($this->returnValue(true)); 806 $c->expects($this->at(1)) 807 ->method('update_state') 808 ->with($cm, COMPLETION_INCOMPLETE, 31337) 809 ->will($this->returnValue(true)); 810 $c->inform_grade_changed($cm, $item, $grade, true); 811 } 812 813 public function test_internal_get_grade_state() { 814 $this->mock_setup(); 815 816 $item = new stdClass; 817 $grade = new stdClass; 818 819 $item->gradepass = 4; 820 $item->hidden = 0; 821 $grade->rawgrade = 4.0; 822 $grade->finalgrade = null; 823 824 // Grade has pass mark and is not hidden, user passes. 825 $this->assertEquals( 826 COMPLETION_COMPLETE_PASS, 827 completion_info::internal_get_grade_state($item, $grade)); 828 829 // Same but user fails. 830 $grade->rawgrade = 3.9; 831 $this->assertEquals( 832 COMPLETION_COMPLETE_FAIL, 833 completion_info::internal_get_grade_state($item, $grade)); 834 835 // User fails on raw grade but passes on final. 836 $grade->finalgrade = 4.0; 837 $this->assertEquals( 838 COMPLETION_COMPLETE_PASS, 839 completion_info::internal_get_grade_state($item, $grade)); 840 841 // Item is hidden. 842 $item->hidden = 1; 843 $this->assertEquals( 844 COMPLETION_COMPLETE, 845 completion_info::internal_get_grade_state($item, $grade)); 846 847 // Item isn't hidden but has no pass mark. 848 $item->hidden = 0; 849 $item->gradepass = 0; 850 $this->assertEquals( 851 COMPLETION_COMPLETE, 852 completion_info::internal_get_grade_state($item, $grade)); 853 } 854 855 public function test_get_activities() { 856 global $CFG; 857 $this->resetAfterTest(); 858 859 // Enable completion before creating modules, otherwise the completion data is not written in DB. 860 $CFG->enablecompletion = true; 861 862 // Create a course with mixed auto completion data. 863 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true)); 864 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 865 $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL); 866 $completionnone = array('completion' => COMPLETION_TRACKING_NONE); 867 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto); 868 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto); 869 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual); 870 871 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone); 872 $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone); 873 $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone); 874 875 // Create data in another course to make sure it's not considered. 876 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true)); 877 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto); 878 $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual); 879 $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone); 880 881 $c = new completion_info($course); 882 $activities = $c->get_activities(); 883 $this->assertCount(3, $activities); 884 $this->assertTrue(isset($activities[$forum->cmid])); 885 $this->assertSame($forum->name, $activities[$forum->cmid]->name); 886 $this->assertTrue(isset($activities[$page->cmid])); 887 $this->assertSame($page->name, $activities[$page->cmid]->name); 888 $this->assertTrue(isset($activities[$data->cmid])); 889 $this->assertSame($data->name, $activities[$data->cmid]->name); 890 891 $this->assertFalse(isset($activities[$forum2->cmid])); 892 $this->assertFalse(isset($activities[$page2->cmid])); 893 $this->assertFalse(isset($activities[$data2->cmid])); 894 } 895 896 public function test_has_activities() { 897 global $CFG; 898 $this->resetAfterTest(); 899 900 // Enable completion before creating modules, otherwise the completion data is not written in DB. 901 $CFG->enablecompletion = true; 902 903 // Create a course with mixed auto completion data. 904 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true)); 905 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true)); 906 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 907 $completionnone = array('completion' => COMPLETION_TRACKING_NONE); 908 $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto); 909 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone); 910 911 $c1 = new completion_info($course); 912 $c2 = new completion_info($course2); 913 914 $this->assertTrue($c1->has_activities()); 915 $this->assertFalse($c2->has_activities()); 916 } 917 918 /** 919 * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses 920 * 921 * @return void 922 */ 923 public function test_course_delete_prerequisite() { 924 global $DB; 925 926 $this->setup_data(); 927 928 $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]); 929 930 $criteriadata = (object) [ 931 'id' => $this->course->id, 932 'criteria_course' => [$courseprerequisite->id], 933 ]; 934 935 /** @var completion_criteria_course $criteria */ 936 $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]); 937 $criteria->update_config($criteriadata); 938 939 // Sanity test. 940 $this->assertTrue($DB->record_exists('course_completion_criteria', [ 941 'course' => $this->course->id, 942 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE, 943 'courseinstance' => $courseprerequisite->id, 944 ])); 945 946 // Deleting the prerequisite course should remove the completion criteria. 947 delete_course($courseprerequisite, false); 948 949 $this->assertFalse($DB->record_exists('course_completion_criteria', [ 950 'course' => $this->course->id, 951 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE, 952 'courseinstance' => $courseprerequisite->id, 953 ])); 954 } 955 956 /** 957 * Test course module completion update event. 958 */ 959 public function test_course_module_completion_updated_event() { 960 global $USER, $CFG; 961 962 $this->setup_data(); 963 964 $this->setAdminUser(); 965 966 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 967 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto); 968 969 $c = new completion_info($this->course); 970 $activities = $c->get_activities(); 971 $this->assertEquals(1, count($activities)); 972 $this->assertTrue(isset($activities[$forum->cmid])); 973 $this->assertEquals($activities[$forum->cmid]->name, $forum->name); 974 975 $current = $c->get_data($activities[$forum->cmid], false, $this->user->id); 976 $current->completionstate = COMPLETION_COMPLETE; 977 $current->timemodified = time(); 978 $sink = $this->redirectEvents(); 979 $c->internal_set_data($activities[$forum->cmid], $current); 980 $events = $sink->get_events(); 981 $event = reset($events); 982 $this->assertInstanceOf('\core\event\course_module_completion_updated', $event); 983 $this->assertEquals($forum->cmid, $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid); 984 $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid)); 985 $this->assertEquals(context_module::instance($forum->cmid), $event->get_context()); 986 $this->assertEquals($USER->id, $event->userid); 987 $this->assertEquals($this->user->id, $event->relateduserid); 988 $this->assertInstanceOf('moodle_url', $event->get_url()); 989 $this->assertEventLegacyData($current, $event); 990 } 991 992 /** 993 * Test course completed event. 994 */ 995 public function test_course_completed_event() { 996 global $USER; 997 998 $this->setup_data(); 999 $this->setAdminUser(); 1000 1001 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 1002 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id)); 1003 1004 // Mark course as complete and get triggered event. 1005 $sink = $this->redirectEvents(); 1006 $ccompletion->mark_complete(); 1007 $events = $sink->get_events(); 1008 $event = reset($events); 1009 1010 $this->assertInstanceOf('\core\event\course_completed', $event); 1011 $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course); 1012 $this->assertEquals($this->course->id, $event->courseid); 1013 $this->assertEquals($USER->id, $event->userid); 1014 $this->assertEquals($this->user->id, $event->relateduserid); 1015 $this->assertEquals(context_course::instance($this->course->id), $event->get_context()); 1016 $this->assertInstanceOf('moodle_url', $event->get_url()); 1017 $data = $ccompletion->get_record_data(); 1018 $this->assertEventLegacyData($data, $event); 1019 } 1020 1021 /** 1022 * Test course completed message. 1023 */ 1024 public function test_course_completed_message() { 1025 $this->setup_data(); 1026 $this->setAdminUser(); 1027 1028 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 1029 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id)); 1030 1031 // Mark course as complete and get the message. 1032 $sink = $this->redirectMessages(); 1033 $ccompletion->mark_complete(); 1034 $messages = $sink->get_messages(); 1035 $sink->close(); 1036 1037 $this->assertCount(1, $messages); 1038 $message = array_pop($messages); 1039 1040 $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom); 1041 $this->assertEquals($this->user->id, $message->useridto); 1042 $this->assertEquals('coursecompleted', $message->eventtype); 1043 $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject); 1044 $this->assertStringContainsString($this->course->fullname, $message->fullmessage); 1045 } 1046 1047 /** 1048 * Test course completed event. 1049 */ 1050 public function test_course_completion_updated_event() { 1051 $this->setup_data(); 1052 $coursecontext = context_course::instance($this->course->id); 1053 $coursecompletionevent = \core\event\course_completion_updated::create( 1054 array( 1055 'courseid' => $this->course->id, 1056 'context' => $coursecontext 1057 ) 1058 ); 1059 1060 // Mark course as complete and get triggered event. 1061 $sink = $this->redirectEvents(); 1062 $coursecompletionevent->trigger(); 1063 $events = $sink->get_events(); 1064 $event = array_pop($events); 1065 $sink->close(); 1066 1067 $this->assertInstanceOf('\core\event\course_completion_updated', $event); 1068 $this->assertEquals($this->course->id, $event->courseid); 1069 $this->assertEquals($coursecontext, $event->get_context()); 1070 $this->assertInstanceOf('moodle_url', $event->get_url()); 1071 $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id); 1072 $this->assertEventLegacyLogData($expectedlegacylog, $event); 1073 } 1074 1075 public function test_completion_can_view_data() { 1076 $this->setup_data(); 1077 1078 $student = $this->getDataGenerator()->create_user(); 1079 $this->getDataGenerator()->enrol_user($student->id, $this->course->id); 1080 1081 $this->setUser($student); 1082 $this->assertTrue(completion_can_view_data($student->id, $this->course->id)); 1083 $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id)); 1084 } 1085} 1086 1087class core_completionlib_fake_recordset implements Iterator { 1088 protected $closed; 1089 protected $values, $index; 1090 1091 public function __construct($values) { 1092 $this->values = $values; 1093 $this->index = 0; 1094 } 1095 1096 public function current() { 1097 return $this->values[$this->index]; 1098 } 1099 1100 public function key() { 1101 return $this->values[$this->index]; 1102 } 1103 1104 public function next() { 1105 $this->index++; 1106 } 1107 1108 public function rewind() { 1109 $this->index = 0; 1110 } 1111 1112 public function valid() { 1113 return count($this->values) > $this->index; 1114 } 1115 1116 public function close() { 1117 $this->closed = true; 1118 } 1119 1120 public function was_closed() { 1121 return $this->closed; 1122 } 1123} 1124