Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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  }