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 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   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   * Course related unit tests
  19   *
  20   * @package    core_course
  21   * @copyright  2014 Marina Glancy
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   * @coversDefaultClass \core_courseformat\base
  24   */
  25  class base_test extends advanced_testcase {
  26  
  27      /**
  28       * Setup to ensure that fixtures are loaded.
  29       */
  30      public static function setupBeforeClass(): void {
  31          global $CFG;
  32          require_once($CFG->dirroot . '/course/lib.php');
  33          require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest.php');
  34          require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_output_course_format_state.php');
  35          require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_output_course_format_invalidoutput.php');
  36      }
  37  
  38      /**
  39       * Tests the save and load functionality.
  40       *
  41       * @author Jason den Dulk
  42       * @covers \core_courseformat
  43       */
  44      public function test_courseformat_saveandload() {
  45          $this->resetAfterTest();
  46  
  47          $courseformatoptiondata = (object) [
  48              "hideoddsections" => 1,
  49              'summary_editor' => [
  50                  'text' => '<p>Somewhere over the rainbow</p><p>The <b>quick</b> brown fox jumpos over the lazy dog.</p>',
  51                  'format' => 1
  52              ]
  53          ];
  54          $generator = $this->getDataGenerator();
  55          $course1 = $generator->create_course(array('format' => 'theunittest'));
  56          $this->assertEquals('theunittest', $course1->format);
  57          course_create_sections_if_missing($course1, array(0, 1));
  58  
  59          $courseformat = course_get_format($course1);
  60          $courseformat->update_course_format_options($courseformatoptiondata);
  61  
  62          $savedcourseformatoptiondata = $courseformat->get_format_options();
  63  
  64          $this->assertEqualsCanonicalizing($courseformatoptiondata, (object) $savedcourseformatoptiondata);
  65      }
  66  
  67      public function test_available_hook() {
  68          global $DB;
  69          $this->resetAfterTest();
  70  
  71          // Generate a course with two sections (0 and 1) and two modules. Course format is set to 'theunittest'.
  72          $generator = $this->getDataGenerator();
  73          $course1 = $generator->create_course(array('format' => 'theunittest'));
  74          $this->assertEquals('theunittest', $course1->format);
  75          course_create_sections_if_missing($course1, array(0, 1));
  76          $assign0 = $generator->create_module('assign', array('course' => $course1, 'section' => 0));
  77          $assign1 = $generator->create_module('assign', array('course' => $course1, 'section' => 1));
  78          $assign2 = $generator->create_module('assign', array('course' => $course1, 'section' => 0, 'visible' => 0));
  79  
  80          // Create a courseoverview role based on the student role.
  81          $roleattr = array('name' => 'courseoverview', 'shortname' => 'courseoverview', 'archetype' => 'student');
  82          $generator->create_role($roleattr);
  83  
  84          // Create user student, editingteacher, teacher and courseoverview.
  85          $student = $generator->create_user();
  86          $teacher = $generator->create_user();
  87          $editingteacher = $generator->create_user();
  88          $courseoverviewuser = $generator->create_user();
  89  
  90          // Enrol users into their roles.
  91          $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
  92          $generator->enrol_user($student->id, $course1->id, $roleids['student']);
  93          $generator->enrol_user($teacher->id, $course1->id, $roleids['teacher']);
  94          $generator->enrol_user($editingteacher->id, $course1->id, $roleids['editingteacher']);
  95          $generator->enrol_user($courseoverviewuser->id, $course1->id, $roleids['courseoverview']);
  96  
  97          // Remove the ignoreavailabilityrestrictions from the teacher role.
  98          role_change_permission($roleids['teacher'], context_system::instance(0),
  99                  'moodle/course:ignoreavailabilityrestrictions', CAP_PREVENT);
 100  
 101          // Allow the courseoverview role to ingore available restriction.
 102          role_change_permission($roleids['courseoverview'], context_system::instance(0),
 103                  'moodle/course:ignoreavailabilityrestrictions', CAP_ALLOW);
 104  
 105          // Make sure that initially both sections and both modules are available and visible for a student.
 106          $modinfostudent = get_fast_modinfo($course1, $student->id);
 107          $this->assertTrue($modinfostudent->get_section_info(1)->available);
 108          $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->available);
 109          $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->uservisible);
 110          $this->assertTrue($modinfostudent->get_cm($assign1->cmid)->available);
 111          $this->assertTrue($modinfostudent->get_cm($assign1->cmid)->uservisible);
 112          $this->assertFalse($modinfostudent->get_cm($assign2->cmid)->uservisible);
 113  
 114          // Set 'hideoddsections' for the course to 1.
 115          // Section1 and assign1 will be unavailable, uservisible will be false for student and true for teacher.
 116          $data = (object)array('id' => $course1->id, 'hideoddsections' => 1);
 117          course_get_format($course1)->update_course_format_options($data);
 118          $modinfostudent = get_fast_modinfo($course1, $student->id);
 119          $this->assertFalse($modinfostudent->get_section_info(1)->available);
 120          $this->assertEmpty($modinfostudent->get_section_info(1)->availableinfo);
 121          $this->assertFalse($modinfostudent->get_section_info(1)->uservisible);
 122          $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->available);
 123          $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->uservisible);
 124          $this->assertFalse($modinfostudent->get_cm($assign1->cmid)->available);
 125          $this->assertFalse($modinfostudent->get_cm($assign1->cmid)->uservisible);
 126          $this->assertFalse($modinfostudent->get_cm($assign2->cmid)->uservisible);
 127  
 128          $modinfoteacher = get_fast_modinfo($course1, $teacher->id);
 129          $this->assertFalse($modinfoteacher->get_section_info(1)->available);
 130          $this->assertEmpty($modinfoteacher->get_section_info(1)->availableinfo);
 131          $this->assertFalse($modinfoteacher->get_section_info(1)->uservisible);
 132          $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->available);
 133          $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->uservisible);
 134          $this->assertFalse($modinfoteacher->get_cm($assign1->cmid)->available);
 135          $this->assertFalse($modinfoteacher->get_cm($assign1->cmid)->uservisible);
 136          $this->assertTrue($modinfoteacher->get_cm($assign2->cmid)->available);
 137          $this->assertTrue($modinfoteacher->get_cm($assign2->cmid)->uservisible);
 138  
 139          $modinfoteacher = get_fast_modinfo($course1, $editingteacher->id);
 140          $this->assertFalse($modinfoteacher->get_section_info(1)->available);
 141          $this->assertEmpty($modinfoteacher->get_section_info(1)->availableinfo);
 142          $this->assertTrue($modinfoteacher->get_section_info(1)->uservisible);
 143          $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->available);
 144          $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->uservisible);
 145          $this->assertFalse($modinfoteacher->get_cm($assign1->cmid)->available);
 146          $this->assertTrue($modinfoteacher->get_cm($assign1->cmid)->uservisible);
 147          $this->assertTrue($modinfoteacher->get_cm($assign2->cmid)->uservisible);
 148  
 149          $modinfocourseoverview = get_fast_modinfo($course1, $courseoverviewuser->id);
 150          $this->assertFalse($modinfocourseoverview->get_section_info(1)->available);
 151          $this->assertEmpty($modinfocourseoverview->get_section_info(1)->availableinfo);
 152          $this->assertTrue($modinfocourseoverview->get_section_info(1)->uservisible);
 153          $this->assertTrue($modinfocourseoverview->get_cm($assign0->cmid)->available);
 154          $this->assertTrue($modinfocourseoverview->get_cm($assign0->cmid)->uservisible);
 155          $this->assertFalse($modinfocourseoverview->get_cm($assign1->cmid)->available);
 156          $this->assertTrue($modinfocourseoverview->get_cm($assign1->cmid)->uservisible);
 157          $this->assertFalse($modinfocourseoverview->get_cm($assign2->cmid)->uservisible);
 158  
 159          // Set 'hideoddsections' for the course to 2.
 160          // Section1 and assign1 will be unavailable, uservisible will be false for student and true for teacher.
 161          // Property availableinfo will be not empty.
 162          $data = (object)array('id' => $course1->id, 'hideoddsections' => 2);
 163          course_get_format($course1)->update_course_format_options($data);
 164          $modinfostudent = get_fast_modinfo($course1, $student->id);
 165          $this->assertFalse($modinfostudent->get_section_info(1)->available);
 166          $this->assertNotEmpty($modinfostudent->get_section_info(1)->availableinfo);
 167          $this->assertFalse($modinfostudent->get_section_info(1)->uservisible);
 168          $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->available);
 169          $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->uservisible);
 170          $this->assertFalse($modinfostudent->get_cm($assign1->cmid)->available);
 171          $this->assertFalse($modinfostudent->get_cm($assign1->cmid)->uservisible);
 172  
 173          $modinfoteacher = get_fast_modinfo($course1, $editingteacher->id);
 174          $this->assertFalse($modinfoteacher->get_section_info(1)->available);
 175          $this->assertNotEmpty($modinfoteacher->get_section_info(1)->availableinfo);
 176          $this->assertTrue($modinfoteacher->get_section_info(1)->uservisible);
 177          $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->available);
 178          $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->uservisible);
 179          $this->assertFalse($modinfoteacher->get_cm($assign1->cmid)->available);
 180          $this->assertTrue($modinfoteacher->get_cm($assign1->cmid)->uservisible);
 181      }
 182  
 183      /**
 184       * Test for supports_news() with a course format plugin that doesn't define 'news_items' in default blocks.
 185       */
 186      public function test_supports_news() {
 187          $this->resetAfterTest();
 188          $format = course_get_format((object)['format' => 'testformat']);
 189          $this->assertFalse($format->supports_news());
 190      }
 191  
 192      /**
 193       * Test for supports_news() for old course format plugins that defines 'news_items' in default blocks.
 194       */
 195      public function test_supports_news_legacy() {
 196          $this->resetAfterTest();
 197          $format = course_get_format((object)['format' => 'testlegacy']);
 198          $this->assertTrue($format->supports_news());
 199      }
 200  
 201      /**
 202       * Test for get_view_url() to ensure that the url is only given for the correct cases
 203       */
 204      public function test_get_view_url() {
 205          global $CFG;
 206          $this->resetAfterTest();
 207  
 208          $linkcoursesections = $CFG->linkcoursesections;
 209  
 210          // Generate a course with two sections (0 and 1) and two modules. Course format is set to 'testformat'.
 211          // This will allow us to test the default implementation of get_view_url.
 212          $generator = $this->getDataGenerator();
 213          $course1 = $generator->create_course(array('format' => 'testformat'));
 214          course_create_sections_if_missing($course1, array(0, 1));
 215  
 216          $data = (object)['id' => $course1->id];
 217          $format = course_get_format($course1);
 218          $format->update_course_format_options($data);
 219  
 220          // In page.
 221          $CFG->linkcoursesections = 0;
 222          $this->assertNotEmpty($format->get_view_url(null));
 223          $this->assertNotEmpty($format->get_view_url(0));
 224          $this->assertNotEmpty($format->get_view_url(1));
 225          $CFG->linkcoursesections = 1;
 226          $this->assertNotEmpty($format->get_view_url(null));
 227          $this->assertNotEmpty($format->get_view_url(0));
 228          $this->assertNotEmpty($format->get_view_url(1));
 229  
 230          // Navigation.
 231          $CFG->linkcoursesections = 0;
 232          $this->assertNull($format->get_view_url(1, ['navigation' => 1]));
 233          $this->assertNull($format->get_view_url(0, ['navigation' => 1]));
 234          $CFG->linkcoursesections = 1;
 235          $this->assertNotEmpty($format->get_view_url(1, ['navigation' => 1]));
 236          $this->assertNotEmpty($format->get_view_url(0, ['navigation' => 1]));
 237      }
 238  
 239      /**
 240       * Test for get_output_classname method.
 241       *
 242       * @dataProvider get_output_classname_provider
 243       * @param string $find the class to find
 244       * @param string $result the expected result classname
 245       * @param bool $exception if the method will raise an exception
 246       */
 247      public function test_get_output_classname($find, $result, $exception) {
 248          $this->resetAfterTest();
 249  
 250          $course = $this->getDataGenerator()->create_course(['format' => 'theunittest']);
 251          $courseformat = course_get_format($course);
 252  
 253          if ($exception) {
 254              $this->expectException(coding_exception::class);
 255          }
 256  
 257          $courseclass = $courseformat->get_output_classname($find);
 258          $this->assertEquals($result, $courseclass);
 259      }
 260  
 261      /**
 262       * Data provider for test_get_output_classname.
 263       *
 264       * @return array the testing scenarios
 265       */
 266      public function get_output_classname_provider(): array {
 267          return [
 268              'overridden class' => [
 269                  'find' => 'state\\course',
 270                  'result' => 'format_theunittest\\output\\courseformat\\state\\course',
 271                  'exception' => false,
 272              ],
 273              'original class' => [
 274                  'find' => 'state\\section',
 275                  'result' => 'core_courseformat\\output\\local\\state\\section',
 276                  'exception' => false,
 277              ],
 278              'invalid overridden class' => [
 279                  'find' => 'state\\invalidoutput',
 280                  'result' => '',
 281                  'exception' => true,
 282              ],
 283          ];
 284      }
 285  
 286      /**
 287       * Test for the default delete format data behaviour.
 288       *
 289       * @covers ::get_sections_preferences
 290       */
 291      public function test_get_sections_preferences() {
 292          $this->resetAfterTest();
 293          $generator = $this->getDataGenerator();
 294          $course = $generator->create_course();
 295          $user = $generator->create_and_enrol($course, 'student');
 296  
 297          // Create fake preferences generated by the frontend js module.
 298          $data = (object)[
 299              'pref1' => [1,2],
 300              'pref2' => [1],
 301          ];
 302          set_user_preference('coursesectionspreferences_' . $course->id, json_encode($data), $user->id);
 303  
 304          $format = course_get_format($course);
 305  
 306          // Load data from user 1.
 307          $this->setUser($user);
 308          $preferences = $format->get_sections_preferences();
 309  
 310          $this->assertEquals(
 311              (object)['pref1' => true, 'pref2' => true],
 312              $preferences[1]
 313          );
 314          $this->assertEquals(
 315              (object)['pref1' => true],
 316              $preferences[2]
 317          );
 318      }
 319  
 320      /**
 321       * Test for the default delete format data behaviour.
 322       *
 323       * @covers ::set_sections_preference
 324       */
 325      public function test_set_sections_preference() {
 326          $this->resetAfterTest();
 327          $generator = $this->getDataGenerator();
 328          $course = $generator->create_course();
 329          $user = $generator->create_and_enrol($course, 'student');
 330  
 331          $format = course_get_format($course);
 332          $this->setUser($user);
 333  
 334          // Load data from user 1.
 335          $format->set_sections_preference('pref1', [1, 2]);
 336          $format->set_sections_preference('pref2', [1]);
 337          $format->set_sections_preference('pref3', []);
 338  
 339          $preferences = $format->get_sections_preferences();
 340          $this->assertEquals(
 341              (object)['pref1' => true, 'pref2' => true],
 342              $preferences[1]
 343          );
 344          $this->assertEquals(
 345              (object)['pref1' => true],
 346              $preferences[2]
 347          );
 348      }
 349  
 350      /**
 351       * Test that retrieving last section number for a course
 352       *
 353       * @covers ::get_last_section_number
 354       */
 355      public function test_get_last_section_number(): void {
 356          global $DB;
 357  
 358          $this->resetAfterTest();
 359  
 360          // Course with two additional sections.
 361          $courseone = $this->getDataGenerator()->create_course(['numsections' => 2]);
 362          $this->assertEquals(2, course_get_format($courseone)->get_last_section_number());
 363  
 364          // Course without additional sections, section zero is the "default" section that always exists.
 365          $coursetwo = $this->getDataGenerator()->create_course(['numsections' => 0]);
 366          $this->assertEquals(0, course_get_format($coursetwo)->get_last_section_number());
 367  
 368          // Course without additional sections, manually remove section zero, as "course_delete_section" prevents that. This
 369          // simulates course data integrity issues that previously triggered errors.
 370          $coursethree = $this->getDataGenerator()->create_course(['numsections' => 0]);
 371          $DB->delete_records('course_sections', ['course' => $coursethree->id, 'section' => 0]);
 372  
 373          $this->assertEquals(-1, course_get_format($coursethree)->get_last_section_number());
 374      }
 375  
 376      /**
 377       * Test for the default delete format data behaviour.
 378       *
 379       * @covers ::delete_format_data
 380       * @dataProvider delete_format_data_provider
 381       * @param bool $usehook if it should use course_delete to trigger $format->delete_format_data as a hook
 382       */
 383      public function test_delete_format_data(bool $usehook) {
 384          global $DB;
 385  
 386          $this->resetAfterTest();
 387  
 388          $generator = $this->getDataGenerator();
 389          $course = $generator->create_course();
 390          course_create_sections_if_missing($course, [0, 1]);
 391          $user = $generator->create_and_enrol($course, 'student');
 392  
 393          // Create a coursesectionspreferences_XX preference.
 394          $key = 'coursesectionspreferences_' . $course->id;
 395          $fakevalue = 'No dark sarcasm in the classroom';
 396          set_user_preference($key, $fakevalue, $user->id);
 397          $this->assertEquals(
 398              $fakevalue,
 399              $DB->get_field('user_preferences', 'value', ['name' => $key, 'userid' => $user->id])
 400          );
 401  
 402          // Create another random user preference.
 403          $key2 = 'somepreference';
 404          $fakevalue2 = "All in all it's just another brick in the wall";
 405          set_user_preference($key2, $fakevalue2, $user->id);
 406          $this->assertEquals(
 407              $fakevalue2,
 408              $DB->get_field('user_preferences', 'value', ['name' => $key2, 'userid' => $user->id])
 409          );
 410  
 411          if ($usehook) {
 412              delete_course($course, false);
 413          } else {
 414              $format = course_get_format($course);
 415              $format->delete_format_data();
 416          }
 417  
 418          // Check which the preferences exists.
 419          $this->assertFalse(
 420              $DB->record_exists('user_preferences', ['name' => $key, 'userid' => $user->id])
 421          );
 422          set_user_preference($key2, $fakevalue2, $user->id);
 423          $this->assertEquals(
 424              $fakevalue2,
 425              $DB->get_field('user_preferences', 'value', ['name' => $key2, 'userid' => $user->id])
 426          );
 427      }
 428  
 429      /**
 430       * Data provider for test_delete_format_data.
 431       *
 432       * @return array the testing scenarios
 433       */
 434      public function delete_format_data_provider(): array {
 435          return [
 436              'direct call' => [
 437                  'usehook' => false
 438              ],
 439              'use hook' => [
 440                  'usehook' => true,
 441              ]
 442          ];
 443      }
 444  }
 445  
 446  /**
 447   * Class format_testformat.
 448   *
 449   * A test class that simulates a course format that doesn't define 'news_items' in default blocks.
 450   *
 451   * @copyright 2016 Jun Pataleta <jun@moodle.com>
 452   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 453   */
 454  class format_testformat extends core_courseformat\base {
 455      /**
 456       * Returns the list of blocks to be automatically added for the newly created course.
 457       *
 458       * @return array
 459       */
 460      public function get_default_blocks() {
 461          return [
 462              BLOCK_POS_RIGHT => [],
 463              BLOCK_POS_LEFT => []
 464          ];
 465      }
 466  }
 467  
 468  /**
 469   * Class format_testlegacy.
 470   *
 471   * A test class that simulates old course formats that define 'news_items' in default blocks.
 472   *
 473   * @copyright 2016 Jun Pataleta <jun@moodle.com>
 474   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 475   */
 476  class format_testlegacy extends core_courseformat\base {
 477      /**
 478       * Returns the list of blocks to be automatically added for the newly created course.
 479       *
 480       * @return array
 481       */
 482      public function get_default_blocks() {
 483          return [
 484              BLOCK_POS_RIGHT => ['news_items'],
 485              BLOCK_POS_LEFT => []
 486          ];
 487      }
 488  }