Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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