Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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