Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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  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          $assign = new \assign(\context_module::instance($assignrow->cmid), false, false);
 206  
 207          // Get basic details.
 208          $modinfo = get_fast_modinfo($course);
 209          $pagecm = $modinfo->get_cm($page->cmid);
 210          $assigncm = $assign->get_course_module();
 211          $info = new \core_availability\mock_info($course, $user->id);
 212  
 213          // COMPLETE state (false), positive and NOT.
 214          $cond = new condition((object)[
 215              'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE
 216          ]);
 217          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 218          $information = $cond->get_description(false, false, $info);
 219          $information = \core_availability\info::format_info($information, $course);
 220          $this->assertMatchesRegularExpression('~Page!.*is marked complete~', $information);
 221          $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 222  
 223          // INCOMPLETE state (true).
 224          $cond = new condition((object)[
 225              'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE
 226          ]);
 227          $this->assertTrue($cond->is_available(false, $info, true, $user->id));
 228          $this->assertFalse($cond->is_available(true, $info, true, $user->id));
 229          $information = $cond->get_description(false, true, $info);
 230          $information = \core_availability\info::format_info($information, $course);
 231          $this->assertMatchesRegularExpression('~Page!.*is marked complete~', $information);
 232  
 233          // Mark page complete.
 234          $completion = new \completion_info($course);
 235          $completion->update_state($pagecm, COMPLETION_COMPLETE);
 236  
 237          // COMPLETE state (true).
 238          $cond = new condition((object)[
 239              'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE
 240          ]);
 241          $this->assertTrue($cond->is_available(false, $info, true, $user->id));
 242          $this->assertFalse($cond->is_available(true, $info, true, $user->id));
 243          $information = $cond->get_description(false, true, $info);
 244          $information = \core_availability\info::format_info($information, $course);
 245          $this->assertMatchesRegularExpression('~Page!.*is incomplete~', $information);
 246  
 247          // INCOMPLETE state (false).
 248          $cond = new condition((object)[
 249              'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE
 250          ]);
 251          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 252          $information = $cond->get_description(false, false, $info);
 253          $information = \core_availability\info::format_info($information, $course);
 254          $this->assertMatchesRegularExpression('~Page!.*is incomplete~', $information);
 255          $this->assertTrue($cond->is_available(true, $info,
 256                  true, $user->id));
 257  
 258          // We are going to need the grade item so that we can get pass/fails.
 259          $gradeitem = $assign->get_grade_item();
 260          \grade_object::set_properties($gradeitem, ['gradepass' => 50.0]);
 261          $gradeitem->update();
 262  
 263          // With no grade, it should return true for INCOMPLETE and false for
 264          // the other three.
 265          $cond = new condition((object)[
 266              'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
 267          ]);
 268          $this->assertTrue($cond->is_available(false, $info, true, $user->id));
 269          $this->assertFalse($cond->is_available(true, $info, true, $user->id));
 270  
 271          $cond = new condition((object)[
 272              'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
 273          ]);
 274          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 275          $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 276  
 277          // Check $information for COMPLETE_PASS and _FAIL as we haven't yet.
 278          $cond = new condition((object)[
 279              'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
 280          ]);
 281          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 282          $information = $cond->get_description(false, false, $info);
 283          $information = \core_availability\info::format_info($information, $course);
 284          $this->assertMatchesRegularExpression('~Assign!.*is complete and passed~', $information);
 285          $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 286  
 287          $cond = new condition((object)[
 288              'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL
 289          ]);
 290          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 291          $information = $cond->get_description(false, false, $info);
 292          $information = \core_availability\info::format_info($information, $course);
 293          $this->assertMatchesRegularExpression('~Assign!.*is complete and failed~', $information);
 294          $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 295  
 296          // Change the grade to be complete and failed.
 297          self::set_grade($assignrow, $user->id, 40);
 298  
 299          $cond = new condition((object)[
 300              'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
 301          ]);
 302          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 303          $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 304  
 305          $cond = new condition((object)[
 306              'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
 307          ]);
 308          $this->assertTrue($cond->is_available(false, $info, true, $user->id));
 309          $this->assertFalse($cond->is_available(true, $info, true, $user->id));
 310  
 311          $cond = new condition((object)[
 312              'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
 313          ]);
 314          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 315          $information = $cond->get_description(false, false, $info);
 316          $information = \core_availability\info::format_info($information, $course);
 317          $this->assertMatchesRegularExpression('~Assign!.*is complete and passed~', $information);
 318          $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 319  
 320          $cond = new condition((object)[
 321              'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL
 322          ]);
 323          $this->assertTrue($cond->is_available(false, $info, true, $user->id));
 324          $this->assertFalse($cond->is_available(true, $info, true, $user->id));
 325          $information = $cond->get_description(false, true, $info);
 326          $information = \core_availability\info::format_info($information, $course);
 327          $this->assertMatchesRegularExpression('~Assign!.*is not complete and failed~', $information);
 328  
 329          // Now change it to pass.
 330          self::set_grade($assignrow, $user->id, 60);
 331  
 332          $cond = new condition((object)[
 333              'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
 334          ]);
 335          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 336          $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 337  
 338          $cond = new condition((object)[
 339              'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
 340          ]);
 341          $this->assertTrue($cond->is_available(false, $info, true, $user->id));
 342          $this->assertFalse($cond->is_available(true, $info, true, $user->id));
 343  
 344          $cond = new condition((object)[
 345                          'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
 346                      ]);
 347          $this->assertTrue($cond->is_available(false, $info, true, $user->id));
 348          $this->assertFalse($cond->is_available(true, $info, true, $user->id));
 349          $information = $cond->get_description(false, true, $info);
 350          $information = \core_availability\info::format_info($information, $course);
 351          $this->assertMatchesRegularExpression('~Assign!.*is not complete and passed~', $information);
 352  
 353          $cond = new condition((object)[
 354              'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL
 355          ]);
 356          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 357          $information = $cond->get_description(false, false, $info);
 358          $information = \core_availability\info::format_info($information, $course);
 359          $this->assertMatchesRegularExpression('~Assign!.*is complete and failed~', $information);
 360          $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 361  
 362          // Simulate deletion of an activity by using an invalid cmid. These
 363          // conditions always fail, regardless of NOT flag or INCOMPLETE.
 364          $cond = new condition((object)[
 365              'cm' => ($assigncm->id + 100), 'e' => COMPLETION_COMPLETE
 366          ]);
 367          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 368          $information = $cond->get_description(false, false, $info);
 369          $information = \core_availability\info::format_info($information, $course);
 370          $this->assertMatchesRegularExpression('~(Missing activity).*is marked complete~', $information);
 371          $this->assertFalse($cond->is_available(true, $info, true, $user->id));
 372          $cond = new condition((object)[
 373              'cm' => ($assigncm->id + 100), 'e' => COMPLETION_INCOMPLETE
 374          ]);
 375          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 376      }
 377  
 378      /**
 379       * Tests the is_available and get_description functions for previous activity option.
 380       *
 381       * @dataProvider previous_activity_data
 382       * @param int $grade the current assign grade (0 for none)
 383       * @param int $condition true for complete, false for incomplete
 384       * @param string $mark activity to mark as complete
 385       * @param string $activity activity name to test
 386       * @param bool $result if it must be available or not
 387       * @param bool $resultnot if it must be available when the condition is inverted
 388       * @param string $description the availabiklity text to check
 389       */
 390      public function test_previous_activity(int $grade, int $condition, string $mark, string $activity,
 391              bool $result, bool $resultnot, string $description): void {
 392          global $CFG, $DB;
 393          require_once($CFG->dirroot . '/mod/assign/locallib.php');
 394          $this->resetAfterTest();
 395  
 396          // Create course with completion turned on.
 397          $CFG->enablecompletion = true;
 398          $CFG->enableavailability = true;
 399          $generator = $this->getDataGenerator();
 400          $course = $generator->create_course(['enablecompletion' => 1]);
 401          $user = $generator->create_user();
 402          $generator->enrol_user($user->id, $course->id);
 403          $this->setUser($user);
 404  
 405          // Page 1 (manual completion).
 406          $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
 407                  ['course' => $course->id, 'name' => 'Page1!',
 408                  'completion' => COMPLETION_TRACKING_MANUAL]);
 409  
 410          // Page 2 (manual completion).
 411          $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
 412                  ['course' => $course->id, 'name' => 'Page2!',
 413                  'completion' => COMPLETION_TRACKING_MANUAL]);
 414  
 415          // Page ignored (no completion).
 416          $pagenocompletion = $generator->get_plugin_generator('mod_page')->create_instance(
 417                  ['course' => $course->id, 'name' => 'Page ignored!']);
 418  
 419          // Create an assignment - we need to have something that can be graded
 420          // so as to test the PASS/FAIL states. Set it up to be completed based
 421          // on its grade item.
 422          $assignrow = $this->getDataGenerator()->create_module('assign', [
 423              'course' => $course->id, 'name' => 'Assign!',
 424              'completion' => COMPLETION_TRACKING_AUTOMATIC
 425          ]);
 426          $DB->set_field('course_modules', 'completiongradeitemnumber', 0,
 427                  ['id' => $assignrow->cmid]);
 428          $assign = new \assign(\context_module::instance($assignrow->cmid), false, false);
 429  
 430          // Page 3 (manual completion).
 431          $page3 = $generator->get_plugin_generator('mod_page')->create_instance(
 432                  ['course' => $course->id, 'name' => 'Page3!',
 433                  'completion' => COMPLETION_TRACKING_MANUAL]);
 434  
 435          // Get basic details.
 436          $activities = [];
 437          $modinfo = get_fast_modinfo($course);
 438          $activities['page1'] = $modinfo->get_cm($page1->cmid);
 439          $activities['page2'] = $modinfo->get_cm($page2->cmid);
 440          $activities['assign'] = $assign->get_course_module();
 441          $activities['page3'] = $modinfo->get_cm($page3->cmid);
 442          $prevvalue = condition::OPTION_PREVIOUS;
 443  
 444          // Setup gradings and completion.
 445          if ($grade) {
 446              $gradeitem = $assign->get_grade_item();
 447              \grade_object::set_properties($gradeitem, ['gradepass' => 50.0]);
 448              $gradeitem->update();
 449              self::set_grade($assignrow, $user->id, $grade);
 450          }
 451          if ($mark) {
 452              $completion = new \completion_info($course);
 453              $completion->update_state($activities[$mark], COMPLETION_COMPLETE);
 454          }
 455  
 456          // Set opprevious WITH non existent previous activity.
 457          $info = new \core_availability\mock_info_module($user->id, $activities[$activity]);
 458          $cond = new condition((object)[
 459              'cm' => (int)$prevvalue, 'e' => $condition
 460          ]);
 461  
 462          // Do the checks.
 463          $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id));
 464          $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id));
 465          $information = $cond->get_description(false, false, $info);
 466          $information = \core_availability\info::format_info($information, $course);
 467          $this->assertMatchesRegularExpression($description, $information);
 468      }
 469  
 470      public function previous_activity_data(): array {
 471          // Assign grade, condition, activity to complete, activity to test, result, resultnot, description.
 472          return [
 473              'Missing previous activity complete' => [
 474                  0, COMPLETION_COMPLETE, '', 'page1', false, false, '~Missing activity.*is marked complete~'
 475              ],
 476              'Missing previous activity incomplete' => [
 477                  0, COMPLETION_INCOMPLETE, '', 'page1', false, false, '~Missing activity.*is incomplete~'
 478              ],
 479              'Previous complete condition with previous activity incompleted' => [
 480                  0, COMPLETION_COMPLETE, '', 'page2', false, true, '~Page1!.*is marked complete~'
 481              ],
 482              'Previous incomplete condition with previous activity incompleted' => [
 483                  0, COMPLETION_INCOMPLETE, '', 'page2', true, false, '~Page1!.*is incomplete~'
 484              ],
 485              'Previous complete condition with previous activity completed' => [
 486                  0, COMPLETION_COMPLETE, 'page1', 'page2', true, false, '~Page1!.*is marked complete~'
 487              ],
 488              'Previous incomplete condition with previous activity completed' => [
 489                  0, COMPLETION_INCOMPLETE, 'page1', 'page2', false, true, '~Page1!.*is incomplete~'
 490              ],
 491              // Depenging on page pass fail (pages are not gradable).
 492              'Previous complete pass condition with previous no gradable activity incompleted' => [
 493                  0, COMPLETION_COMPLETE_PASS, '', 'page2', false, true, '~Page1!.*is complete and passed~'
 494              ],
 495              'Previous complete fail condition with previous no gradable activity incompleted' => [
 496                  0, COMPLETION_COMPLETE_FAIL, '', 'page2', false, true, '~Page1!.*is complete and failed~'
 497              ],
 498              'Previous complete pass condition with previous no gradable activity completed' => [
 499                  0, COMPLETION_COMPLETE_PASS, 'page1', 'page2', false, true, '~Page1!.*is complete and passed~'
 500              ],
 501              'Previous complete fail condition with previous no gradable activity completed' => [
 502                  0, COMPLETION_COMPLETE_FAIL, 'page1', 'page2', false, true, '~Page1!.*is complete and failed~'
 503              ],
 504              // There's an page without completion between page2 ans assign.
 505              'Previous complete condition with sibling activity incompleted' => [
 506                  0, COMPLETION_COMPLETE, '', 'assign', false, true, '~Page2!.*is marked complete~'
 507              ],
 508              'Previous incomplete condition with sibling activity incompleted' => [
 509                  0, COMPLETION_INCOMPLETE, '', 'assign', true, false, '~Page2!.*is incomplete~'
 510              ],
 511              'Previous complete condition with sibling activity completed' => [
 512                  0, COMPLETION_COMPLETE, 'page2', 'assign', true, false, '~Page2!.*is marked complete~'
 513              ],
 514              'Previous incomplete condition with sibling activity completed' => [
 515                  0, COMPLETION_INCOMPLETE, 'page2', 'assign', false, true, '~Page2!.*is incomplete~'
 516              ],
 517              // Depending on assign without grade.
 518              'Previous complete condition with previous without grade' => [
 519                  0, COMPLETION_COMPLETE, '', 'page3', false, true, '~Assign!.*is marked complete~'
 520              ],
 521              'Previous incomplete condition with previous without grade' => [
 522                  0, COMPLETION_INCOMPLETE, '', 'page3', true, false, '~Assign!.*is incomplete~'
 523              ],
 524              'Previous complete pass condition with previous without grade' => [
 525                  0, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~'
 526              ],
 527              'Previous complete fail condition with previous without grade' => [
 528                  0, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~'
 529              ],
 530              // Depending on assign with grade.
 531              'Previous complete condition with previous fail grade' => [
 532                  40, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~'
 533              ],
 534              'Previous incomplete condition with previous fail grade' => [
 535                  40, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~'
 536              ],
 537              'Previous complete pass condition with previous fail grade' => [
 538                  40, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~'
 539              ],
 540              'Previous complete fail condition with previous fail grade' => [
 541                  40, COMPLETION_COMPLETE_FAIL, '', 'page3', true, false, '~Assign!.*is complete and failed~'
 542              ],
 543              'Previous complete condition with previous pass grade' => [
 544                  60, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~'
 545              ],
 546              'Previous incomplete condition with previous pass grade' => [
 547                  60, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~'
 548              ],
 549              'Previous complete pass condition with previous pass grade' => [
 550                  60, COMPLETION_COMPLETE_PASS, '', 'page3', true, false, '~Assign!.*is complete and passed~'
 551              ],
 552              'Previous complete fail condition with previous pass grade' => [
 553                  60, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~'
 554              ],
 555          ];
 556      }
 557  
 558      /**
 559       * Tests the is_available and get_description functions for
 560       * previous activity option in course sections.
 561       *
 562       * @dataProvider section_previous_activity_data
 563       * @param int $condition condition value
 564       * @param bool $mark if Page 1 must be mark as completed
 565       * @param string $section section to add the availability
 566       * @param bool $result expected result
 567       * @param bool $resultnot expected negated result
 568       * @param string $description description to match
 569       */
 570      public function test_section_previous_activity(int $condition, bool $mark, string $section,
 571                  bool $result, bool $resultnot, string $description): void {
 572          global $CFG, $DB;
 573          require_once($CFG->dirroot . '/mod/assign/locallib.php');
 574          $this->resetAfterTest();
 575  
 576          // Create course with completion turned on.
 577          $CFG->enablecompletion = true;
 578          $CFG->enableavailability = true;
 579          $generator = $this->getDataGenerator();
 580          $course = $generator->create_course(
 581                  ['numsections' => 4, 'enablecompletion' => 1],
 582                  ['createsections' => true]);
 583          $user = $generator->create_user();
 584          $generator->enrol_user($user->id, $course->id);
 585          $this->setUser($user);
 586  
 587          // Section 1 - page1 (manual completion).
 588          $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
 589                  ['course' => $course->id, 'name' => 'Page1!', 'section' => 1,
 590                  'completion' => COMPLETION_TRACKING_MANUAL]);
 591  
 592          // Section 1 - page ignored 1 (no completion).
 593          $pagenocompletion1 = $generator->get_plugin_generator('mod_page')->create_instance(
 594                  ['course' => $course, 'name' => 'Page ignored!', 'section' => 1]);
 595  
 596          // Section 2 - page ignored 2 (no completion).
 597          $pagenocompletion2 = $generator->get_plugin_generator('mod_page')->create_instance(
 598                  ['course' => $course, 'name' => 'Page ignored!', 'section' => 2]);
 599  
 600          // Section 3 - page2 (manual completion).
 601          $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
 602                  ['course' => $course->id, 'name' => 'Page2!', 'section' => 3,
 603                  'completion' => COMPLETION_TRACKING_MANUAL]);
 604  
 605          // Section 4 is empty.
 606  
 607          // Get basic details.
 608          get_fast_modinfo(0, 0, true);
 609          $modinfo = get_fast_modinfo($course);
 610          $sections['section1'] = $modinfo->get_section_info(1);
 611          $sections['section2'] = $modinfo->get_section_info(2);
 612          $sections['section3'] = $modinfo->get_section_info(3);
 613          $sections['section4'] = $modinfo->get_section_info(4);
 614          $page1cm = $modinfo->get_cm($page1->cmid);
 615          $prevvalue = condition::OPTION_PREVIOUS;
 616  
 617          if ($mark) {
 618              // Mark page1 complete.
 619              $completion = new \completion_info($course);
 620              $completion->update_state($page1cm, COMPLETION_COMPLETE);
 621          }
 622  
 623          $info = new \core_availability\mock_info_section($user->id, $sections[$section]);
 624          $cond = new condition((object)[
 625              'cm' => (int)$prevvalue, 'e' => $condition
 626          ]);
 627          $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id));
 628          $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id));
 629          $information = $cond->get_description(false, false, $info);
 630          $information = \core_availability\info::format_info($information, $course);
 631          $this->assertMatchesRegularExpression($description, $information);
 632  
 633      }
 634  
 635      public function section_previous_activity_data(): array {
 636          return [
 637              // Condition, Activity completion, section to test, result, resultnot, description.
 638              'Completion complete Section with no previous activity' => [
 639                  COMPLETION_COMPLETE, false, 'section1', false, false, '~Missing activity.*is marked complete~'
 640              ],
 641              'Completion incomplete Section with no previous activity' => [
 642                  COMPLETION_INCOMPLETE, false, 'section1', false, false, '~Missing activity.*is incomplete~'
 643              ],
 644              // Section 2 depending on section 1 -> Page 1 (no grading).
 645              'Completion complete Section with previous activity incompleted' => [
 646                  COMPLETION_COMPLETE, false, 'section2', false, true, '~Page1!.*is marked complete~'
 647              ],
 648              'Completion incomplete Section with previous activity incompleted' => [
 649                  COMPLETION_INCOMPLETE, false, 'section2', true, false, '~Page1!.*is incomplete~'
 650              ],
 651              'Completion complete Section with previous activity completed' => [
 652                  COMPLETION_COMPLETE, true, 'section2', true, false, '~Page1!.*is marked complete~'
 653              ],
 654              'Completion incomplete Section with previous activity completed' => [
 655                  COMPLETION_INCOMPLETE, true, 'section2', false, true, '~Page1!.*is incomplete~'
 656              ],
 657              // Section 3 depending on section 1 -> Page 1 (no grading).
 658              'Completion complete Section ignoring empty sections and activity incompleted' => [
 659                  COMPLETION_COMPLETE, false, 'section3', false, true, '~Page1!.*is marked complete~'
 660              ],
 661              'Completion incomplete Section ignoring empty sections and activity incompleted' => [
 662                  COMPLETION_INCOMPLETE, false, 'section3', true, false, '~Page1!.*is incomplete~'
 663              ],
 664              'Completion complete Section ignoring empty sections and activity completed' => [
 665                  COMPLETION_COMPLETE, true, 'section3', true, false, '~Page1!.*is marked complete~'
 666              ],
 667              'Completion incomplete Section ignoring empty sections and activity completed' => [
 668                  COMPLETION_INCOMPLETE, true, 'section3', false, true, '~Page1!.*is incomplete~'
 669              ],
 670              // Section 4 depending on section 3 -> Page 2 (no grading).
 671              'Completion complete Last section with previous activity incompleted' => [
 672                  COMPLETION_COMPLETE, false, 'section4', false, true, '~Page2!.*is marked complete~'
 673              ],
 674              'Completion incomplete Last section with previous activity incompleted' => [
 675                  COMPLETION_INCOMPLETE, false, 'section4', true, false, '~Page2!.*is incomplete~'
 676              ],
 677              'Completion complete Last section with previous activity completed' => [
 678                  COMPLETION_COMPLETE, true, 'section4', false, true, '~Page2!.*is marked complete~'
 679              ],
 680              'Completion incomplete Last section with previous activity completed' => [
 681                  COMPLETION_INCOMPLETE, true, 'section4', true, false, '~Page2!.*is incomplete~'
 682              ],
 683          ];
 684      }
 685  
 686      /**
 687       * Tests completion_value_used static function.
 688       */
 689      public function test_completion_value_used() {
 690          global $CFG, $DB;
 691          $this->resetAfterTest();
 692          $prevvalue = condition::OPTION_PREVIOUS;
 693  
 694          // Create course with completion turned on and some sections.
 695          $CFG->enablecompletion = true;
 696          $CFG->enableavailability = true;
 697          $generator = $this->getDataGenerator();
 698          $course = $generator->create_course(
 699                  ['numsections' => 1, 'enablecompletion' => 1],
 700                  ['createsections' => true]);
 701  
 702          // Create six pages with manual completion.
 703          $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
 704                  ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
 705          $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
 706                  ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
 707          $page3 = $generator->get_plugin_generator('mod_page')->create_instance(
 708                  ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
 709          $page4 = $generator->get_plugin_generator('mod_page')->create_instance(
 710                  ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
 711          $page5 = $generator->get_plugin_generator('mod_page')->create_instance(
 712                  ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
 713          $page6 = $generator->get_plugin_generator('mod_page')->create_instance(
 714                  ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
 715  
 716          // Set up page3 to depend on page1, and section1 to depend on page2.
 717          $DB->set_field('course_modules', 'availability',
 718                  '{"op":"|","show":true,"c":[' .
 719                  '{"type":"completion","e":1,"cm":' . $page1->cmid . '}]}',
 720                  ['id' => $page3->cmid]);
 721          $DB->set_field('course_sections', 'availability',
 722                  '{"op":"|","show":true,"c":[' .
 723                  '{"type":"completion","e":1,"cm":' . $page2->cmid . '}]}',
 724                  ['course' => $course->id, 'section' => 1]);
 725          // Set up page5 and page6 to depend on previous activity.
 726          $DB->set_field('course_modules', 'availability',
 727                  '{"op":"|","show":true,"c":[' .
 728                  '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}',
 729                  ['id' => $page5->cmid]);
 730          $DB->set_field('course_modules', 'availability',
 731                  '{"op":"|","show":true,"c":[' .
 732                  '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}',
 733                  ['id' => $page6->cmid]);
 734  
 735          // Check 1: nothing depends on page3 and page6 but something does on the others.
 736          $this->assertTrue(condition::completion_value_used(
 737                  $course, $page1->cmid));
 738          $this->assertTrue(condition::completion_value_used(
 739                  $course, $page2->cmid));
 740          $this->assertFalse(condition::completion_value_used(
 741                  $course, $page3->cmid));
 742          $this->assertTrue(condition::completion_value_used(
 743                  $course, $page4->cmid));
 744          $this->assertTrue(condition::completion_value_used(
 745                  $course, $page5->cmid));
 746          $this->assertFalse(condition::completion_value_used(
 747                  $course, $page6->cmid));
 748      }
 749  
 750      /**
 751       * Updates the grade of a user in the given assign module instance.
 752       *
 753       * @param \stdClass $assignrow Assignment row from database
 754       * @param int $userid User id
 755       * @param float $grade Grade
 756       */
 757      protected static function set_grade($assignrow, $userid, $grade) {
 758          $grades = [];
 759          $grades[$userid] = (object)[
 760              'rawgrade' => $grade, 'userid' => $userid
 761          ];
 762          $assignrow->cmidnumber = null;
 763          assign_grade_item_update($assignrow, $grades);
 764      }
 765  
 766      /**
 767       * Tests the update_dependency_id() function.
 768       */
 769      public function test_update_dependency_id() {
 770          $cond = new condition((object)[
 771              'cm' => 42, 'e' => COMPLETION_COMPLETE, 'selfid' => 43
 772          ]);
 773          $this->assertFalse($cond->update_dependency_id('frogs', 42, 540));
 774          $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
 775          $this->assertTrue($cond->update_dependency_id('course_modules', 42, 456));
 776          $after = $cond->save();
 777          $this->assertEquals(456, $after->cm);
 778  
 779          // Test selfid updating.
 780          $cond = new condition((object)[
 781              'cm' => 42, 'e' => COMPLETION_COMPLETE
 782          ]);
 783          $this->assertFalse($cond->update_dependency_id('frogs', 43, 540));
 784          $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
 785          $after = $cond->save();
 786          $this->assertEquals(42, $after->cm);
 787  
 788          // Test on previous activity.
 789          $cond = new condition((object)[
 790              'cm' => condition::OPTION_PREVIOUS,
 791              'e' => COMPLETION_COMPLETE
 792          ]);
 793          $this->assertFalse($cond->update_dependency_id('frogs', 43, 80));
 794          $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
 795          $after = $cond->save();
 796          $this->assertEquals(condition::OPTION_PREVIOUS, $after->cm);
 797      }
 798  }