See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
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 namespace availability_completion; 18 19 defined('MOODLE_INTERNAL') || die(); 20 21 global $CFG; 22 require_once($CFG->libdir . '/completionlib.php'); 23 24 /** 25 * Unit tests for the completion condition. 26 * 27 * @package availability_completion 28 * @copyright 2014 The Open University 29 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 */ 31 class condition_test extends \advanced_testcase { 32 33 /** 34 * Setup to ensure that fixtures are loaded. 35 */ 36 public static function setupBeforeClass(): void { 37 global $CFG; 38 // Load the mock info class so that it can be used. 39 require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php'); 40 require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info_module.php'); 41 require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info_section.php'); 42 } 43 44 /** 45 * Load required classes. 46 */ 47 public function setUp(): void { 48 condition::wipe_static_cache(); 49 } 50 51 /** 52 * Tests constructing and using condition as part of tree. 53 */ 54 public function test_in_tree() { 55 global $USER, $CFG; 56 $this->resetAfterTest(); 57 58 $this->setAdminUser(); 59 60 // Create course with completion turned on and a Page. 61 $CFG->enablecompletion = true; 62 $CFG->enableavailability = true; 63 $generator = $this->getDataGenerator(); 64 $course = $generator->create_course(['enablecompletion' => 1]); 65 $page = $generator->get_plugin_generator('mod_page')->create_instance( 66 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); 67 $selfpage = $generator->get_plugin_generator('mod_page')->create_instance( 68 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); 69 70 $modinfo = get_fast_modinfo($course); 71 $cm = $modinfo->get_cm($page->cmid); 72 $info = new \core_availability\mock_info($course, $USER->id); 73 74 $structure = (object)[ 75 'op' => '|', 76 'show' => true, 77 'c' => [ 78 (object)[ 79 'type' => 'completion', 80 'cm' => (int)$cm->id, 81 'e' => COMPLETION_COMPLETE 82 ] 83 ] 84 ]; 85 $tree = new \core_availability\tree($structure); 86 87 // Initial check (user has not completed activity). 88 $result = $tree->check_available(false, $info, true, $USER->id); 89 $this->assertFalse($result->is_available()); 90 91 // Mark activity complete. 92 $completion = new \completion_info($course); 93 $completion->update_state($cm, COMPLETION_COMPLETE); 94 95 // Now it's true! 96 $result = $tree->check_available(false, $info, true, $USER->id); 97 $this->assertTrue($result->is_available()); 98 } 99 100 /** 101 * Tests the constructor including error conditions. Also tests the 102 * string conversion feature (intended for debugging only). 103 */ 104 public function test_constructor() { 105 // No parameters. 106 $structure = new \stdClass(); 107 try { 108 $cond = new condition($structure); 109 $this->fail(); 110 } catch (\coding_exception $e) { 111 $this->assertStringContainsString('Missing or invalid ->cm', $e->getMessage()); 112 } 113 114 // Invalid $cm. 115 $structure->cm = 'hello'; 116 try { 117 $cond = new condition($structure); 118 $this->fail(); 119 } catch (\coding_exception $e) { 120 $this->assertStringContainsString('Missing or invalid ->cm', $e->getMessage()); 121 } 122 123 // Missing $e. 124 $structure->cm = 42; 125 try { 126 $cond = new condition($structure); 127 $this->fail(); 128 } catch (\coding_exception $e) { 129 $this->assertStringContainsString('Missing or invalid ->e', $e->getMessage()); 130 } 131 132 // Invalid $e. 133 $structure->e = 99; 134 try { 135 $cond = new condition($structure); 136 $this->fail(); 137 } catch (\coding_exception $e) { 138 $this->assertStringContainsString('Missing or invalid ->e', $e->getMessage()); 139 } 140 141 // Successful construct & display with all different expected values. 142 $structure->e = COMPLETION_COMPLETE; 143 $cond = new condition($structure); 144 $this->assertEquals('{completion:cm42 COMPLETE}', (string)$cond); 145 146 $structure->e = COMPLETION_COMPLETE_PASS; 147 $cond = new condition($structure); 148 $this->assertEquals('{completion:cm42 COMPLETE_PASS}', (string)$cond); 149 150 $structure->e = COMPLETION_COMPLETE_FAIL; 151 $cond = new condition($structure); 152 $this->assertEquals('{completion:cm42 COMPLETE_FAIL}', (string)$cond); 153 154 $structure->e = COMPLETION_INCOMPLETE; 155 $cond = new condition($structure); 156 $this->assertEquals('{completion:cm42 INCOMPLETE}', (string)$cond); 157 158 // Successful contruct with previous activity. 159 $structure->cm = condition::OPTION_PREVIOUS; 160 $cond = new condition($structure); 161 $this->assertEquals('{completion:cmopprevious INCOMPLETE}', (string)$cond); 162 163 } 164 165 /** 166 * Tests the save() function. 167 */ 168 public function test_save() { 169 $structure = (object)['cm' => 42, 'e' => COMPLETION_COMPLETE]; 170 $cond = new condition($structure); 171 $structure->type = 'completion'; 172 $this->assertEquals($structure, $cond->save()); 173 } 174 175 /** 176 * Tests the is_available and get_description functions. 177 */ 178 public function test_usage() { 179 global $CFG, $DB; 180 require_once($CFG->dirroot . '/mod/assign/locallib.php'); 181 $this->resetAfterTest(); 182 183 // Create course with completion turned on. 184 $CFG->enablecompletion = true; 185 $CFG->enableavailability = true; 186 $generator = $this->getDataGenerator(); 187 $course = $generator->create_course(['enablecompletion' => 1]); 188 $user = $generator->create_user(); 189 $generator->enrol_user($user->id, $course->id); 190 $this->setUser($user); 191 192 // Create a Page with manual completion for basic checks. 193 $page = $generator->get_plugin_generator('mod_page')->create_instance( 194 ['course' => $course->id, 'name' => 'Page!', 195 'completion' => COMPLETION_TRACKING_MANUAL]); 196 197 // Create an assignment - we need to have something that can be graded 198 // so as to test the PASS/FAIL states. Set it up to be completed based 199 // on its grade item. 200 $assignrow = $this->getDataGenerator()->create_module('assign', [ 201 'course' => $course->id, 'name' => 'Assign!', 202 'completion' => COMPLETION_TRACKING_AUTOMATIC]); 203 $DB->set_field('course_modules', 'completiongradeitemnumber', 0, 204 ['id' => $assignrow->cmid]); 205 // As we manually set the field here, we are going to need to reset the modinfo cache. 206 rebuild_course_cache($course->id, true); 207 $assign = new \assign(\context_module::instance($assignrow->cmid), false, false); 208 209 // Get basic details. 210 $modinfo = get_fast_modinfo($course); 211 $pagecm = $modinfo->get_cm($page->cmid); 212 $assigncm = $assign->get_course_module(); 213 $info = new \core_availability\mock_info($course, $user->id); 214 215 // COMPLETE state (false), positive and NOT. 216 $cond = new condition((object)[ 217 'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE 218 ]); 219 $this->assertFalse($cond->is_available(false, $info, true, $user->id)); 220 $information = $cond->get_description(false, false, $info); 221 $information = \core_availability\info::format_info($information, $course); 222 $this->assertMatchesRegularExpression('~Page!.*is marked complete~', $information); 223 $this->assertTrue($cond->is_available(true, $info, true, $user->id)); 224 225 // INCOMPLETE state (true). 226 $cond = new condition((object)[ 227 'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE 228 ]); 229 $this->assertTrue($cond->is_available(false, $info, true, $user->id)); 230 $this->assertFalse($cond->is_available(true, $info, true, $user->id)); 231 $information = $cond->get_description(false, true, $info); 232 $information = \core_availability\info::format_info($information, $course); 233 $this->assertMatchesRegularExpression('~Page!.*is marked complete~', $information); 234 235 // Mark page complete. 236 $completion = new \completion_info($course); 237 $completion->update_state($pagecm, COMPLETION_COMPLETE); 238 239 // COMPLETE state (true). 240 $cond = new condition((object)[ 241 'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE 242 ]); 243 $this->assertTrue($cond->is_available(false, $info, true, $user->id)); 244 $this->assertFalse($cond->is_available(true, $info, true, $user->id)); 245 $information = $cond->get_description(false, true, $info); 246 $information = \core_availability\info::format_info($information, $course); 247 $this->assertMatchesRegularExpression('~Page!.*is incomplete~', $information); 248 249 // INCOMPLETE state (false). 250 $cond = new condition((object)[ 251 'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE 252 ]); 253 $this->assertFalse($cond->is_available(false, $info, true, $user->id)); 254 $information = $cond->get_description(false, false, $info); 255 $information = \core_availability\info::format_info($information, $course); 256 $this->assertMatchesRegularExpression('~Page!.*is incomplete~', $information); 257 $this->assertTrue($cond->is_available(true, $info, 258 true, $user->id)); 259 260 // We are going to need the grade item so that we can get pass/fails. 261 $gradeitem = $assign->get_grade_item(); 262 \grade_object::set_properties($gradeitem, ['gradepass' => 50.0]); 263 $gradeitem->update(); 264 265 // With no grade, it should return true for INCOMPLETE and false for 266 // the other three. 267 $cond = new condition((object)[ 268 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE 269 ]); 270 $this->assertTrue($cond->is_available(false, $info, true, $user->id)); 271 $this->assertFalse($cond->is_available(true, $info, true, $user->id)); 272 273 $cond = new condition((object)[ 274 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE 275 ]); 276 $this->assertFalse($cond->is_available(false, $info, true, $user->id)); 277 $this->assertTrue($cond->is_available(true, $info, true, $user->id)); 278 279 // Check $information for COMPLETE_PASS and _FAIL as we haven't yet. 280 $cond = new condition((object)[ 281 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS 282 ]); 283 $this->assertFalse($cond->is_available(false, $info, true, $user->id)); 284 $information = $cond->get_description(false, false, $info); 285 $information = \core_availability\info::format_info($information, $course); 286 $this->assertMatchesRegularExpression('~Assign!.*is complete and passed~', $information); 287 $this->assertTrue($cond->is_available(true, $info, true, $user->id)); 288 289 $cond = new condition((object)[ 290 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL 291 ]); 292 $this->assertFalse($cond->is_available(false, $info, true, $user->id)); 293 $information = $cond->get_description(false, false, $info); 294 $information = \core_availability\info::format_info($information, $course); 295 $this->assertMatchesRegularExpression('~Assign!.*is complete and failed~', $information); 296 $this->assertTrue($cond->is_available(true, $info, true, $user->id)); 297 298 // Change the grade to be complete and failed. 299 self::set_grade($assignrow, $user->id, 40); 300 301 $cond = new condition((object)[ 302 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE 303 ]); 304 $this->assertTrue($cond->is_available(false, $info, true, $user->id)); 305 $this->assertFalse($cond->is_available(true, $info, true, $user->id)); 306 307 $cond = new condition((object)[ 308 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE 309 ]); 310 $this->assertFalse($cond->is_available(false, $info, true, $user->id)); 311 $this->assertTrue($cond->is_available(true, $info, true, $user->id)); 312 313 $cond = new condition((object)[ 314 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS 315 ]); 316 $this->assertFalse($cond->is_available(false, $info, true, $user->id)); 317 $information = $cond->get_description(false, false, $info); 318 $information = \core_availability\info::format_info($information, $course); 319 $this->assertMatchesRegularExpression('~Assign!.*is complete and passed~', $information); 320 $this->assertTrue($cond->is_available(true, $info, true, $user->id)); 321 322 $cond = new condition((object)[ 323 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL 324 ]); 325 $this->assertTrue($cond->is_available(false, $info, true, $user->id)); 326 $this->assertFalse($cond->is_available(true, $info, true, $user->id)); 327 $information = $cond->get_description(false, true, $info); 328 $information = \core_availability\info::format_info($information, $course); 329 $this->assertMatchesRegularExpression('~Assign!.*is not complete and failed~', $information); 330 331 // Now change it to pass. 332 self::set_grade($assignrow, $user->id, 60); 333 334 $cond = new condition((object)[ 335 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE 336 ]); 337 $this->assertFalse($cond->is_available(false, $info, true, $user->id)); 338 $this->assertTrue($cond->is_available(true, $info, true, $user->id)); 339 340 $cond = new condition((object)[ 341 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE 342 ]); 343 $this->assertTrue($cond->is_available(false, $info, true, $user->id)); 344 $this->assertFalse($cond->is_available(true, $info, true, $user->id)); 345 346 $cond = new condition((object)[ 347 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS 348 ]); 349 $this->assertTrue($cond->is_available(false, $info, true, $user->id)); 350 $this->assertFalse($cond->is_available(true, $info, true, $user->id)); 351 $information = $cond->get_description(false, true, $info); 352 $information = \core_availability\info::format_info($information, $course); 353 $this->assertMatchesRegularExpression('~Assign!.*is not complete and passed~', $information); 354 355 $cond = new condition((object)[ 356 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL 357 ]); 358 $this->assertFalse($cond->is_available(false, $info, true, $user->id)); 359 $information = $cond->get_description(false, false, $info); 360 $information = \core_availability\info::format_info($information, $course); 361 $this->assertMatchesRegularExpression('~Assign!.*is complete and failed~', $information); 362 $this->assertTrue($cond->is_available(true, $info, true, $user->id)); 363 364 // Simulate deletion of an activity by using an invalid cmid. These 365 // conditions always fail, regardless of NOT flag or INCOMPLETE. 366 $cond = new condition((object)[ 367 'cm' => ($assigncm->id + 100), 'e' => COMPLETION_COMPLETE 368 ]); 369 $this->assertFalse($cond->is_available(false, $info, true, $user->id)); 370 $information = $cond->get_description(false, false, $info); 371 $information = \core_availability\info::format_info($information, $course); 372 $this->assertMatchesRegularExpression('~(Missing activity).*is marked complete~', $information); 373 $this->assertFalse($cond->is_available(true, $info, true, $user->id)); 374 $cond = new condition((object)[ 375 'cm' => ($assigncm->id + 100), 'e' => COMPLETION_INCOMPLETE 376 ]); 377 $this->assertFalse($cond->is_available(false, $info, true, $user->id)); 378 } 379 380 /** 381 * Tests the is_available and get_description functions for previous activity option. 382 * 383 * @dataProvider previous_activity_data 384 * @param int $grade the current assign grade (0 for none) 385 * @param int $condition true for complete, false for incomplete 386 * @param string $mark activity to mark as complete 387 * @param string $activity activity name to test 388 * @param bool $result if it must be available or not 389 * @param bool $resultnot if it must be available when the condition is inverted 390 * @param string $description the availabiklity text to check 391 */ 392 public function test_previous_activity(int $grade, int $condition, string $mark, string $activity, 393 bool $result, bool $resultnot, string $description): void { 394 global $CFG, $DB; 395 require_once($CFG->dirroot . '/mod/assign/locallib.php'); 396 $this->resetAfterTest(); 397 398 // Create course with completion turned on. 399 $CFG->enablecompletion = true; 400 $CFG->enableavailability = true; 401 $generator = $this->getDataGenerator(); 402 $course = $generator->create_course(['enablecompletion' => 1]); 403 $user = $generator->create_user(); 404 $generator->enrol_user($user->id, $course->id); 405 $this->setUser($user); 406 407 // Page 1 (manual completion). 408 $page1 = $generator->get_plugin_generator('mod_page')->create_instance( 409 ['course' => $course->id, 'name' => 'Page1!', 410 'completion' => COMPLETION_TRACKING_MANUAL]); 411 412 // Page 2 (manual completion). 413 $page2 = $generator->get_plugin_generator('mod_page')->create_instance( 414 ['course' => $course->id, 'name' => 'Page2!', 415 'completion' => COMPLETION_TRACKING_MANUAL]); 416 417 // Page ignored (no completion). 418 $pagenocompletion = $generator->get_plugin_generator('mod_page')->create_instance( 419 ['course' => $course->id, 'name' => 'Page ignored!']); 420 421 // Create an assignment - we need to have something that can be graded 422 // so as to test the PASS/FAIL states. Set it up to be completed based 423 // on its grade item. 424 $assignrow = $this->getDataGenerator()->create_module('assign', [ 425 'course' => $course->id, 'name' => 'Assign!', 426 'completion' => COMPLETION_TRACKING_AUTOMATIC 427 ]); 428 $DB->set_field('course_modules', 'completiongradeitemnumber', 0, 429 ['id' => $assignrow->cmid]); 430 $assign = new \assign(\context_module::instance($assignrow->cmid), false, false); 431 432 // Page 3 (manual completion). 433 $page3 = $generator->get_plugin_generator('mod_page')->create_instance( 434 ['course' => $course->id, 'name' => 'Page3!', 435 'completion' => COMPLETION_TRACKING_MANUAL]); 436 437 // Get basic details. 438 $activities = []; 439 $modinfo = get_fast_modinfo($course); 440 $activities['page1'] = $modinfo->get_cm($page1->cmid); 441 $activities['page2'] = $modinfo->get_cm($page2->cmid); 442 $activities['assign'] = $assign->get_course_module(); 443 $activities['page3'] = $modinfo->get_cm($page3->cmid); 444 $prevvalue = condition::OPTION_PREVIOUS; 445 446 // Setup gradings and completion. 447 if ($grade) { 448 $gradeitem = $assign->get_grade_item(); 449 \grade_object::set_properties($gradeitem, ['gradepass' => 50.0]); 450 $gradeitem->update(); 451 self::set_grade($assignrow, $user->id, $grade); 452 } 453 if ($mark) { 454 $completion = new \completion_info($course); 455 $completion->update_state($activities[$mark], COMPLETION_COMPLETE); 456 } 457 458 // Set opprevious WITH non existent previous activity. 459 $info = new \core_availability\mock_info_module($user->id, $activities[$activity]); 460 $cond = new condition((object)[ 461 'cm' => (int)$prevvalue, 'e' => $condition 462 ]); 463 464 // Do the checks. 465 $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id)); 466 $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id)); 467 $information = $cond->get_description(false, false, $info); 468 $information = \core_availability\info::format_info($information, $course); 469 $this->assertMatchesRegularExpression($description, $information); 470 } 471 472 public function previous_activity_data(): array { 473 // Assign grade, condition, activity to complete, activity to test, result, resultnot, description. 474 return [ 475 'Missing previous activity complete' => [ 476 0, COMPLETION_COMPLETE, '', 'page1', false, false, '~Missing activity.*is marked complete~' 477 ], 478 'Missing previous activity incomplete' => [ 479 0, COMPLETION_INCOMPLETE, '', 'page1', false, false, '~Missing activity.*is incomplete~' 480 ], 481 'Previous complete condition with previous activity incompleted' => [ 482 0, COMPLETION_COMPLETE, '', 'page2', false, true, '~Page1!.*is marked complete~' 483 ], 484 'Previous incomplete condition with previous activity incompleted' => [ 485 0, COMPLETION_INCOMPLETE, '', 'page2', true, false, '~Page1!.*is incomplete~' 486 ], 487 'Previous complete condition with previous activity completed' => [ 488 0, COMPLETION_COMPLETE, 'page1', 'page2', true, false, '~Page1!.*is marked complete~' 489 ], 490 'Previous incomplete condition with previous activity completed' => [ 491 0, COMPLETION_INCOMPLETE, 'page1', 'page2', false, true, '~Page1!.*is incomplete~' 492 ], 493 // Depenging on page pass fail (pages are not gradable). 494 'Previous complete pass condition with previous no gradable activity incompleted' => [ 495 0, COMPLETION_COMPLETE_PASS, '', 'page2', false, true, '~Page1!.*is complete and passed~' 496 ], 497 'Previous complete fail condition with previous no gradable activity incompleted' => [ 498 0, COMPLETION_COMPLETE_FAIL, '', 'page2', false, true, '~Page1!.*is complete and failed~' 499 ], 500 'Previous complete pass condition with previous no gradable activity completed' => [ 501 0, COMPLETION_COMPLETE_PASS, 'page1', 'page2', false, true, '~Page1!.*is complete and passed~' 502 ], 503 'Previous complete fail condition with previous no gradable activity completed' => [ 504 0, COMPLETION_COMPLETE_FAIL, 'page1', 'page2', false, true, '~Page1!.*is complete and failed~' 505 ], 506 // There's an page without completion between page2 ans assign. 507 'Previous complete condition with sibling activity incompleted' => [ 508 0, COMPLETION_COMPLETE, '', 'assign', false, true, '~Page2!.*is marked complete~' 509 ], 510 'Previous incomplete condition with sibling activity incompleted' => [ 511 0, COMPLETION_INCOMPLETE, '', 'assign', true, false, '~Page2!.*is incomplete~' 512 ], 513 'Previous complete condition with sibling activity completed' => [ 514 0, COMPLETION_COMPLETE, 'page2', 'assign', true, false, '~Page2!.*is marked complete~' 515 ], 516 'Previous incomplete condition with sibling activity completed' => [ 517 0, COMPLETION_INCOMPLETE, 'page2', 'assign', false, true, '~Page2!.*is incomplete~' 518 ], 519 // Depending on assign without grade. 520 'Previous complete condition with previous without grade' => [ 521 0, COMPLETION_COMPLETE, '', 'page3', false, true, '~Assign!.*is marked complete~' 522 ], 523 'Previous incomplete condition with previous without grade' => [ 524 0, COMPLETION_INCOMPLETE, '', 'page3', true, false, '~Assign!.*is incomplete~' 525 ], 526 'Previous complete pass condition with previous without grade' => [ 527 0, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~' 528 ], 529 'Previous complete fail condition with previous without grade' => [ 530 0, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~' 531 ], 532 // Depending on assign with grade. 533 'Previous complete condition with previous fail grade' => [ 534 40, COMPLETION_COMPLETE, '', 'page3', false, true, '~Assign!.*is marked complete~', 535 ], 536 'Previous incomplete condition with previous fail grade' => [ 537 40, COMPLETION_INCOMPLETE, '', 'page3', true, false, '~Assign!.*is incomplete~', 538 ], 539 'Previous complete pass condition with previous fail grade' => [ 540 40, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~' 541 ], 542 'Previous complete fail condition with previous fail grade' => [ 543 40, COMPLETION_COMPLETE_FAIL, '', 'page3', true, false, '~Assign!.*is complete and failed~' 544 ], 545 'Previous complete condition with previous pass grade' => [ 546 60, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~' 547 ], 548 'Previous incomplete condition with previous pass grade' => [ 549 60, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~' 550 ], 551 'Previous complete pass condition with previous pass grade' => [ 552 60, COMPLETION_COMPLETE_PASS, '', 'page3', true, false, '~Assign!.*is complete and passed~' 553 ], 554 'Previous complete fail condition with previous pass grade' => [ 555 60, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~' 556 ], 557 ]; 558 } 559 560 /** 561 * Tests the is_available and get_description functions for 562 * previous activity option in course sections. 563 * 564 * @dataProvider section_previous_activity_data 565 * @param int $condition condition value 566 * @param bool $mark if Page 1 must be mark as completed 567 * @param string $section section to add the availability 568 * @param bool $result expected result 569 * @param bool $resultnot expected negated result 570 * @param string $description description to match 571 */ 572 public function test_section_previous_activity(int $condition, bool $mark, string $section, 573 bool $result, bool $resultnot, string $description): void { 574 global $CFG, $DB; 575 require_once($CFG->dirroot . '/mod/assign/locallib.php'); 576 $this->resetAfterTest(); 577 578 // Create course with completion turned on. 579 $CFG->enablecompletion = true; 580 $CFG->enableavailability = true; 581 $generator = $this->getDataGenerator(); 582 $course = $generator->create_course( 583 ['numsections' => 4, 'enablecompletion' => 1], 584 ['createsections' => true]); 585 $user = $generator->create_user(); 586 $generator->enrol_user($user->id, $course->id); 587 $this->setUser($user); 588 589 // Section 1 - page1 (manual completion). 590 $page1 = $generator->get_plugin_generator('mod_page')->create_instance( 591 ['course' => $course->id, 'name' => 'Page1!', 'section' => 1, 592 'completion' => COMPLETION_TRACKING_MANUAL]); 593 594 // Section 1 - page ignored 1 (no completion). 595 $pagenocompletion1 = $generator->get_plugin_generator('mod_page')->create_instance( 596 ['course' => $course, 'name' => 'Page ignored!', 'section' => 1]); 597 598 // Section 2 - page ignored 2 (no completion). 599 $pagenocompletion2 = $generator->get_plugin_generator('mod_page')->create_instance( 600 ['course' => $course, 'name' => 'Page ignored!', 'section' => 2]); 601 602 // Section 3 - page2 (manual completion). 603 $page2 = $generator->get_plugin_generator('mod_page')->create_instance( 604 ['course' => $course->id, 'name' => 'Page2!', 'section' => 3, 605 'completion' => COMPLETION_TRACKING_MANUAL]); 606 607 // Section 4 is empty. 608 609 // Get basic details. 610 get_fast_modinfo(0, 0, true); 611 $modinfo = get_fast_modinfo($course); 612 $sections['section1'] = $modinfo->get_section_info(1); 613 $sections['section2'] = $modinfo->get_section_info(2); 614 $sections['section3'] = $modinfo->get_section_info(3); 615 $sections['section4'] = $modinfo->get_section_info(4); 616 $page1cm = $modinfo->get_cm($page1->cmid); 617 $prevvalue = condition::OPTION_PREVIOUS; 618 619 if ($mark) { 620 // Mark page1 complete. 621 $completion = new \completion_info($course); 622 $completion->update_state($page1cm, COMPLETION_COMPLETE); 623 } 624 625 $info = new \core_availability\mock_info_section($user->id, $sections[$section]); 626 $cond = new condition((object)[ 627 'cm' => (int)$prevvalue, 'e' => $condition 628 ]); 629 $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id)); 630 $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id)); 631 $information = $cond->get_description(false, false, $info); 632 $information = \core_availability\info::format_info($information, $course); 633 $this->assertMatchesRegularExpression($description, $information); 634 635 } 636 637 public function section_previous_activity_data(): array { 638 return [ 639 // Condition, Activity completion, section to test, result, resultnot, description. 640 'Completion complete Section with no previous activity' => [ 641 COMPLETION_COMPLETE, false, 'section1', false, false, '~Missing activity.*is marked complete~' 642 ], 643 'Completion incomplete Section with no previous activity' => [ 644 COMPLETION_INCOMPLETE, false, 'section1', false, false, '~Missing activity.*is incomplete~' 645 ], 646 // Section 2 depending on section 1 -> Page 1 (no grading). 647 'Completion complete Section with previous activity incompleted' => [ 648 COMPLETION_COMPLETE, false, 'section2', false, true, '~Page1!.*is marked complete~' 649 ], 650 'Completion incomplete Section with previous activity incompleted' => [ 651 COMPLETION_INCOMPLETE, false, 'section2', true, false, '~Page1!.*is incomplete~' 652 ], 653 'Completion complete Section with previous activity completed' => [ 654 COMPLETION_COMPLETE, true, 'section2', true, false, '~Page1!.*is marked complete~' 655 ], 656 'Completion incomplete Section with previous activity completed' => [ 657 COMPLETION_INCOMPLETE, true, 'section2', false, true, '~Page1!.*is incomplete~' 658 ], 659 // Section 3 depending on section 1 -> Page 1 (no grading). 660 'Completion complete Section ignoring empty sections and activity incompleted' => [ 661 COMPLETION_COMPLETE, false, 'section3', false, true, '~Page1!.*is marked complete~' 662 ], 663 'Completion incomplete Section ignoring empty sections and activity incompleted' => [ 664 COMPLETION_INCOMPLETE, false, 'section3', true, false, '~Page1!.*is incomplete~' 665 ], 666 'Completion complete Section ignoring empty sections and activity completed' => [ 667 COMPLETION_COMPLETE, true, 'section3', true, false, '~Page1!.*is marked complete~' 668 ], 669 'Completion incomplete Section ignoring empty sections and activity completed' => [ 670 COMPLETION_INCOMPLETE, true, 'section3', false, true, '~Page1!.*is incomplete~' 671 ], 672 // Section 4 depending on section 3 -> Page 2 (no grading). 673 'Completion complete Last section with previous activity incompleted' => [ 674 COMPLETION_COMPLETE, false, 'section4', false, true, '~Page2!.*is marked complete~' 675 ], 676 'Completion incomplete Last section with previous activity incompleted' => [ 677 COMPLETION_INCOMPLETE, false, 'section4', true, false, '~Page2!.*is incomplete~' 678 ], 679 'Completion complete Last section with previous activity completed' => [ 680 COMPLETION_COMPLETE, true, 'section4', false, true, '~Page2!.*is marked complete~' 681 ], 682 'Completion incomplete Last section with previous activity completed' => [ 683 COMPLETION_INCOMPLETE, true, 'section4', true, false, '~Page2!.*is incomplete~' 684 ], 685 ]; 686 } 687 688 /** 689 * Tests completion_value_used static function. 690 */ 691 public function test_completion_value_used() { 692 global $CFG, $DB; 693 $this->resetAfterTest(); 694 $prevvalue = condition::OPTION_PREVIOUS; 695 696 // Create course with completion turned on and some sections. 697 $CFG->enablecompletion = true; 698 $CFG->enableavailability = true; 699 $generator = $this->getDataGenerator(); 700 $course = $generator->create_course( 701 ['numsections' => 1, 'enablecompletion' => 1], 702 ['createsections' => true]); 703 704 // Create six pages with manual completion. 705 $page1 = $generator->get_plugin_generator('mod_page')->create_instance( 706 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); 707 $page2 = $generator->get_plugin_generator('mod_page')->create_instance( 708 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); 709 $page3 = $generator->get_plugin_generator('mod_page')->create_instance( 710 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); 711 $page4 = $generator->get_plugin_generator('mod_page')->create_instance( 712 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); 713 $page5 = $generator->get_plugin_generator('mod_page')->create_instance( 714 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); 715 $page6 = $generator->get_plugin_generator('mod_page')->create_instance( 716 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); 717 718 // Set up page3 to depend on page1, and section1 to depend on page2. 719 $DB->set_field('course_modules', 'availability', 720 '{"op":"|","show":true,"c":[' . 721 '{"type":"completion","e":1,"cm":' . $page1->cmid . '}]}', 722 ['id' => $page3->cmid]); 723 $DB->set_field('course_sections', 'availability', 724 '{"op":"|","show":true,"c":[' . 725 '{"type":"completion","e":1,"cm":' . $page2->cmid . '}]}', 726 ['course' => $course->id, 'section' => 1]); 727 // Set up page5 and page6 to depend on previous activity. 728 $DB->set_field('course_modules', 'availability', 729 '{"op":"|","show":true,"c":[' . 730 '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}', 731 ['id' => $page5->cmid]); 732 $DB->set_field('course_modules', 'availability', 733 '{"op":"|","show":true,"c":[' . 734 '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}', 735 ['id' => $page6->cmid]); 736 737 // Check 1: nothing depends on page3 and page6 but something does on the others. 738 $this->assertTrue(condition::completion_value_used( 739 $course, $page1->cmid)); 740 $this->assertTrue(condition::completion_value_used( 741 $course, $page2->cmid)); 742 $this->assertFalse(condition::completion_value_used( 743 $course, $page3->cmid)); 744 $this->assertTrue(condition::completion_value_used( 745 $course, $page4->cmid)); 746 $this->assertTrue(condition::completion_value_used( 747 $course, $page5->cmid)); 748 $this->assertFalse(condition::completion_value_used( 749 $course, $page6->cmid)); 750 } 751 752 /** 753 * Updates the grade of a user in the given assign module instance. 754 * 755 * @param \stdClass $assignrow Assignment row from database 756 * @param int $userid User id 757 * @param float $grade Grade 758 */ 759 protected static function set_grade($assignrow, $userid, $grade) { 760 $grades = []; 761 $grades[$userid] = (object)[ 762 'rawgrade' => $grade, 'userid' => $userid 763 ]; 764 $assignrow->cmidnumber = null; 765 assign_grade_item_update($assignrow, $grades); 766 } 767 768 /** 769 * Tests the update_dependency_id() function. 770 */ 771 public function test_update_dependency_id() { 772 $cond = new condition((object)[ 773 'cm' => 42, 'e' => COMPLETION_COMPLETE, 'selfid' => 43 774 ]); 775 $this->assertFalse($cond->update_dependency_id('frogs', 42, 540)); 776 $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34)); 777 $this->assertTrue($cond->update_dependency_id('course_modules', 42, 456)); 778 $after = $cond->save(); 779 $this->assertEquals(456, $after->cm); 780 781 // Test selfid updating. 782 $cond = new condition((object)[ 783 'cm' => 42, 'e' => COMPLETION_COMPLETE 784 ]); 785 $this->assertFalse($cond->update_dependency_id('frogs', 43, 540)); 786 $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34)); 787 $after = $cond->save(); 788 $this->assertEquals(42, $after->cm); 789 790 // Test on previous activity. 791 $cond = new condition((object)[ 792 'cm' => condition::OPTION_PREVIOUS, 793 'e' => COMPLETION_COMPLETE 794 ]); 795 $this->assertFalse($cond->update_dependency_id('frogs', 43, 80)); 796 $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34)); 797 $after = $cond->save(); 798 $this->assertEquals(condition::OPTION_PREVIOUS, $after->cm); 799 } 800 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body