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]

   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 core_course;
  18  
  19  use core_course_category;
  20  
  21  /**
  22   * Tests for class core_course_category
  23   *
  24   * @package    core_course
  25   * @category   test
  26   * @copyright  2013 Marina Glancy
  27   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  28   */
  29  class category_test extends \advanced_testcase {
  30  
  31      protected $roles;
  32  
  33      protected function setUp(): void {
  34          parent::setUp();
  35          $this->resetAfterTest();
  36          $user = $this->getDataGenerator()->create_user();
  37          $this->setUser($user);
  38      }
  39  
  40      protected function get_roleid($context = null) {
  41          global $USER;
  42          if ($context === null) {
  43              $context = \context_system::instance();
  44          }
  45          if (is_object($context)) {
  46              $context = $context->id;
  47          }
  48          if (empty($this->roles)) {
  49              $this->roles = array();
  50          }
  51          if (empty($this->roles[$USER->id])) {
  52              $this->roles[$USER->id] = array();
  53          }
  54          if (empty($this->roles[$USER->id][$context])) {
  55              $this->roles[$USER->id][$context] = create_role('Role for '.$USER->id.' in '.$context, 'role'.$USER->id.'-'.$context, '-');
  56              role_assign($this->roles[$USER->id][$context], $USER->id, $context);
  57          }
  58          return $this->roles[$USER->id][$context];
  59      }
  60  
  61      protected function assign_capability($capability, $permission = CAP_ALLOW, $contextid = null) {
  62          if ($contextid === null) {
  63              $contextid = \context_system::instance();
  64          }
  65          if (is_object($contextid)) {
  66              $contextid = $contextid->id;
  67          }
  68          assign_capability($capability, $permission, $this->get_roleid($contextid), $contextid, true);
  69          accesslib_clear_all_caches_for_unit_testing();
  70      }
  71  
  72      public function test_create_coursecat() {
  73          // Create the category.
  74          $data = new \stdClass();
  75          $data->name = 'aaa';
  76          $data->description = 'aaa';
  77          $data->idnumber = '';
  78  
  79          $category1 = core_course_category::create($data);
  80  
  81          // Initially confirm that base data was inserted correctly.
  82          $this->assertSame($data->name, $category1->name);
  83          $this->assertSame($data->description, $category1->description);
  84          $this->assertSame($data->idnumber, $category1->idnumber);
  85  
  86          $this->assertGreaterThanOrEqual(1, $category1->sortorder);
  87  
  88          // Create two more categories and test the sortorder worked correctly.
  89          $data->name = 'ccc';
  90          $category2 = core_course_category::create($data);
  91  
  92          $data->name = 'bbb';
  93          $category3 = core_course_category::create($data);
  94  
  95          $this->assertGreaterThan($category1->sortorder, $category2->sortorder);
  96          $this->assertGreaterThan($category2->sortorder, $category3->sortorder);
  97      }
  98  
  99      public function test_name_idnumber_exceptions() {
 100          try {
 101              core_course_category::create(array('name' => ''));
 102              $this->fail('Missing category name exception expected in core_course_category::create');
 103          } catch (\moodle_exception $e) {
 104              $this->assertInstanceOf('moodle_exception', $e);
 105          }
 106          $cat1 = core_course_category::create(array('name' => 'Cat1', 'idnumber' => '1'));
 107          try {
 108              $cat1->update(array('name' => ''));
 109              $this->fail('Missing category name exception expected in core_course_category::update');
 110          } catch (\moodle_exception $e) {
 111              $this->assertInstanceOf('moodle_exception', $e);
 112          }
 113          try {
 114              core_course_category::create(array('name' => 'Cat2', 'idnumber' => '1'));
 115              $this->fail('Duplicate idnumber exception expected in core_course_category::create');
 116          } catch (\moodle_exception $e) {
 117              $this->assertInstanceOf('moodle_exception', $e);
 118          }
 119          $cat2 = core_course_category::create(array('name' => 'Cat2', 'idnumber' => '2'));
 120          try {
 121              $cat2->update(array('idnumber' => '1'));
 122              $this->fail('Duplicate idnumber exception expected in core_course_category::update');
 123          } catch (\moodle_exception $e) {
 124              $this->assertInstanceOf('moodle_exception', $e);
 125          }
 126          // Test that duplicates with an idnumber of 0 cannot be created.
 127          core_course_category::create(array('name' => 'Cat3', 'idnumber' => '0'));
 128          try {
 129              core_course_category::create(array('name' => 'Cat4', 'idnumber' => '0'));
 130              $this->fail('Duplicate idnumber "0" exception expected in core_course_category::create');
 131          } catch (\moodle_exception $e) {
 132              $this->assertInstanceOf('moodle_exception', $e);
 133          }
 134          // Test an update cannot make a duplicate idnumber of 0.
 135          try {
 136              $cat2->update(array('idnumber' => '0'));
 137              $this->fail('Duplicate idnumber "0" exception expected in core_course_category::update');
 138          } catch (\Exception $e) {
 139              $this->assertInstanceOf('moodle_exception', $e);
 140          }
 141      }
 142  
 143      public function test_visibility() {
 144          $this->assign_capability('moodle/category:viewhiddencategories');
 145          $this->assign_capability('moodle/category:manage');
 146  
 147          // Create category 1 initially hidden.
 148          $category1 = core_course_category::create(array('name' => 'Cat1', 'visible' => 0));
 149          $this->assertEquals(0, $category1->visible);
 150          $this->assertEquals(0, $category1->visibleold);
 151  
 152          // Create category 2 initially hidden as a child of hidden category 1.
 153          $category2 = core_course_category::create(array('name' => 'Cat2', 'visible' => 0, 'parent' => $category1->id));
 154          $this->assertEquals(0, $category2->visible);
 155          $this->assertEquals(0, $category2->visibleold);
 156  
 157          // Create category 3 initially visible as a child of hidden category 1.
 158          $category3 = core_course_category::create(array('name' => 'Cat3', 'visible' => 1, 'parent' => $category1->id));
 159          $this->assertEquals(0, $category3->visible);
 160          $this->assertEquals(1, $category3->visibleold);
 161  
 162          // Show category 1 and make sure that category 2 is hidden and category 3 is visible.
 163          $category1->show();
 164          $this->assertEquals(1, core_course_category::get($category1->id)->visible);
 165          $this->assertEquals(0, core_course_category::get($category2->id)->visible);
 166          $this->assertEquals(1, core_course_category::get($category3->id)->visible);
 167  
 168          // Create visible category 4.
 169          $category4 = core_course_category::create(array('name' => 'Cat4'));
 170          $this->assertEquals(1, $category4->visible);
 171          $this->assertEquals(1, $category4->visibleold);
 172  
 173          // Create visible category 5 as a child of visible category 4.
 174          $category5 = core_course_category::create(array('name' => 'Cat5', 'parent' => $category4->id));
 175          $this->assertEquals(1, $category5->visible);
 176          $this->assertEquals(1, $category5->visibleold);
 177  
 178          // Hide category 4 and make sure category 5 is hidden too.
 179          $category4->hide();
 180          $this->assertEquals(0, $category4->visible);
 181          $this->assertEquals(0, $category4->visibleold);
 182          $category5 = core_course_category::get($category5->id); // We have to re-read from DB.
 183          $this->assertEquals(0, $category5->visible);
 184          $this->assertEquals(1, $category5->visibleold);
 185  
 186          // Show category 4 and make sure category 5 is visible too.
 187          $category4->show();
 188          $this->assertEquals(1, $category4->visible);
 189          $this->assertEquals(1, $category4->visibleold);
 190          $category5 = core_course_category::get($category5->id); // We have to re-read from DB.
 191          $this->assertEquals(1, $category5->visible);
 192          $this->assertEquals(1, $category5->visibleold);
 193  
 194          // Move category 5 under hidden category 2 and make sure it became hidden.
 195          $category5->change_parent($category2->id);
 196          $this->assertEquals(0, $category5->visible);
 197          $this->assertEquals(1, $category5->visibleold);
 198  
 199          // Re-read object for category 5 from DB and check again.
 200          $category5 = core_course_category::get($category5->id);
 201          $this->assertEquals(0, $category5->visible);
 202          $this->assertEquals(1, $category5->visibleold);
 203  
 204          // Rricky one! Move hidden category 5 under visible category ("Top") and make sure it is still hidden-
 205          // WHY? Well, different people may expect different behaviour here. So better keep it hidden.
 206          $category5->change_parent(0);
 207          $this->assertEquals(0, $category5->visible);
 208          $this->assertEquals(1, $category5->visibleold);
 209      }
 210  
 211      public function test_hierarchy() {
 212          $this->assign_capability('moodle/category:viewhiddencategories');
 213          $this->assign_capability('moodle/category:manage');
 214  
 215          $category1 = core_course_category::create(array('name' => 'Cat1'));
 216          $category2 = core_course_category::create(array('name' => 'Cat2', 'parent' => $category1->id));
 217          $category3 = core_course_category::create(array('name' => 'Cat3', 'parent' => $category1->id));
 218          $category4 = core_course_category::create(array('name' => 'Cat4', 'parent' => $category2->id));
 219  
 220          // Check function get_children().
 221          $this->assertEquals(array($category2->id, $category3->id), array_keys($category1->get_children()));
 222          // Check function get_parents().
 223          $this->assertEquals(array($category1->id, $category2->id), $category4->get_parents());
 224  
 225          // Can not move category to itself or to it's children.
 226          $this->assertFalse($category1->can_change_parent($category2->id));
 227          $this->assertFalse($category2->can_change_parent($category2->id));
 228          // Can move category to grandparent.
 229          $this->assertTrue($category4->can_change_parent($category1->id));
 230  
 231          try {
 232              $category2->change_parent($category4->id);
 233              $this->fail('Exception expected - can not move category');
 234          } catch (\moodle_exception $e) {
 235              $this->assertInstanceOf('moodle_exception', $e);
 236          }
 237  
 238          $category4->change_parent(0);
 239          $this->assertEquals(array(), $category4->get_parents());
 240          $this->assertEquals(array($category2->id, $category3->id), array_keys($category1->get_children()));
 241          $this->assertEquals(array(), array_keys($category2->get_children()));
 242      }
 243  
 244      public function test_update() {
 245          $category1 = core_course_category::create(array('name' => 'Cat1'));
 246          $timecreated = $category1->timemodified;
 247          $this->assertSame('Cat1', $category1->name);
 248          $this->assertTrue(empty($category1->description));
 249          $this->waitForSecond();
 250          $testdescription = 'This is cat 1 а также русский текст';
 251          $category1->update(array('description' => $testdescription));
 252          $this->assertSame($testdescription, $category1->description);
 253          $category1 = core_course_category::get($category1->id);
 254          $this->assertSame($testdescription, $category1->description);
 255          \cache_helper::purge_by_event('changesincoursecat');
 256          $category1 = core_course_category::get($category1->id);
 257          $this->assertSame($testdescription, $category1->description);
 258  
 259          $this->assertGreaterThan($timecreated, $category1->timemodified);
 260      }
 261  
 262      public function test_delete() {
 263          global $DB;
 264  
 265          $this->assign_capability('moodle/category:manage');
 266          $this->assign_capability('moodle/course:create');
 267  
 268          $initialcatid = $DB->get_field_sql('SELECT max(id) from {course_categories}');
 269  
 270          $category1 = core_course_category::create(array('name' => 'Cat1'));
 271          $category2 = core_course_category::create(array('name' => 'Cat2', 'parent' => $category1->id));
 272          $category3 = core_course_category::create(array('name' => 'Cat3'));
 273          $category4 = core_course_category::create(array('name' => 'Cat4', 'parent' => $category2->id));
 274  
 275          $course1 = $this->getDataGenerator()->create_course(array('category' => $category2->id));
 276          $course2 = $this->getDataGenerator()->create_course(array('category' => $category4->id));
 277          $course3 = $this->getDataGenerator()->create_course(array('category' => $category4->id));
 278          $course4 = $this->getDataGenerator()->create_course(array('category' => $category1->id));
 279  
 280          // Now we have
 281          // $category1
 282          //   $category2
 283          //      $category4
 284          //        $course2
 285          //        $course3
 286          //      $course1
 287          //   $course4
 288          // $category3
 289          // structure.
 290  
 291          // Login as another user to test course:delete capability (user who created course can delete it within 24h even without cap).
 292          $this->setUser($this->getDataGenerator()->create_user());
 293  
 294          // Delete category 2 and move content to category 3.
 295          $this->assertFalse($category2->can_move_content_to($category3->id)); // No luck!
 296          // Add necessary capabilities.
 297          $this->assign_capability('moodle/course:create', CAP_ALLOW, \context_coursecat::instance($category3->id));
 298          $this->assign_capability('moodle/category:manage');
 299          $this->assertTrue($category2->can_move_content_to($category3->id)); // Hurray!
 300          $category2->delete_move($category3->id);
 301  
 302          // Make sure we have:
 303          // $category1
 304          //   $course4
 305          // $category3
 306          //    $category4
 307          //      $course2
 308          //      $course3
 309          //    $course1
 310          // structure.
 311  
 312          $this->assertNull(core_course_category::get($category2->id, IGNORE_MISSING, true));
 313          $this->assertEquals(array(), $category1->get_children());
 314          $this->assertEquals(array($category4->id), array_keys($category3->get_children()));
 315          $this->assertEquals($category4->id, $DB->get_field('course', 'category', array('id' => $course2->id)));
 316          $this->assertEquals($category4->id, $DB->get_field('course', 'category', array('id' => $course3->id)));
 317          $this->assertEquals($category3->id, $DB->get_field('course', 'category', array('id' => $course1->id)));
 318  
 319          // Delete category 3 completely.
 320          $this->assertFalse($category3->can_delete_full()); // No luck!
 321          // Add necessary capabilities.
 322          $this->assign_capability('moodle/course:delete', CAP_ALLOW, \context_coursecat::instance($category3->id));
 323          $this->assertTrue($category3->can_delete_full()); // Hurray!
 324          $category3->delete_full();
 325  
 326          // Make sure we have:
 327          // $category1
 328          //   $course4
 329          // structure.
 330  
 331          // Note that we also have default course category and default 'site' course.
 332          $this->assertEquals(1, $DB->get_field_sql('SELECT count(*) FROM {course_categories} WHERE id > ?', array($initialcatid)));
 333          $this->assertEquals($category1->id, $DB->get_field_sql('SELECT max(id) FROM {course_categories}'));
 334          $this->assertEquals(1, $DB->get_field_sql('SELECT count(*) FROM {course} WHERE id <> ?', array(SITEID)));
 335          $this->assertEquals(array('id' => $course4->id, 'category' => $category1->id),
 336                  (array)$DB->get_record_sql('SELECT id, category from {course} where id <> ?', array(SITEID)));
 337      }
 338  
 339      public function test_get_children() {
 340          $category1 = core_course_category::create(array('name' => 'Cat1'));
 341          $category2 = core_course_category::create(array('name' => 'Cat2', 'parent' => $category1->id));
 342          $category3 = core_course_category::create(array('name' => 'Cat3', 'parent' => $category1->id, 'visible' => 0));
 343          $category4 = core_course_category::create(array('name' => 'Cat4', 'idnumber' => '12', 'parent' => $category1->id));
 344          $category5 = core_course_category::create(array('name' => 'Cat5', 'idnumber' => '11',
 345              'parent' => $category1->id, 'visible' => 0));
 346          $category6 = core_course_category::create(array('name' => 'Cat6', 'idnumber' => '10', 'parent' => $category1->id));
 347          $category7 = core_course_category::create(array('name' => 'Cat0', 'parent' => $category1->id));
 348  
 349          $children = $category1->get_children();
 350          // User does not have the capability to view hidden categories, so the list should be
 351          // 2, 4, 6, 7.
 352          $this->assertEquals(array($category2->id, $category4->id, $category6->id, $category7->id), array_keys($children));
 353          $this->assertEquals(4, $category1->get_children_count());
 354  
 355          $children = $category1->get_children(array('offset' => 2));
 356          $this->assertEquals(array($category6->id, $category7->id), array_keys($children));
 357          $this->assertEquals(4, $category1->get_children_count());
 358  
 359          $children = $category1->get_children(array('limit' => 2));
 360          $this->assertEquals(array($category2->id, $category4->id), array_keys($children));
 361  
 362          $children = $category1->get_children(array('offset' => 1, 'limit' => 2));
 363          $this->assertEquals(array($category4->id, $category6->id), array_keys($children));
 364  
 365          $children = $category1->get_children(array('sort' => array('name' => 1)));
 366          // Must be 7, 2, 4, 6.
 367          $this->assertEquals(array($category7->id, $category2->id, $category4->id, $category6->id), array_keys($children));
 368  
 369          $children = $category1->get_children(array('sort' => array('idnumber' => 1, 'name' => -1)));
 370          // Must be 2, 7, 6, 4.
 371          $this->assertEquals(array($category2->id, $category7->id, $category6->id, $category4->id), array_keys($children));
 372  
 373          // Check that everything is all right after purging the caches.
 374          \cache_helper::purge_by_event('changesincoursecat');
 375          $children = $category1->get_children();
 376          $this->assertEquals(array($category2->id, $category4->id, $category6->id, $category7->id), array_keys($children));
 377          $this->assertEquals(4, $category1->get_children_count());
 378      }
 379  
 380      /**
 381       * Test the get_all_children_ids function.
 382       */
 383      public function test_get_all_children_ids() {
 384          $category1 = core_course_category::create(array('name' => 'Cat1'));
 385          $category2 = core_course_category::create(array('name' => 'Cat2'));
 386          $category11 = core_course_category::create(array('name' => 'Cat11', 'parent' => $category1->id));
 387          $category12 = core_course_category::create(array('name' => 'Cat12', 'parent' => $category1->id));
 388          $category13 = core_course_category::create(array('name' => 'Cat13', 'parent' => $category1->id));
 389          $category111 = core_course_category::create(array('name' => 'Cat111', 'parent' => $category11->id));
 390          $category112 = core_course_category::create(array('name' => 'Cat112', 'parent' => $category11->id));
 391          $category1121 = core_course_category::create(array('name' => 'Cat1121', 'parent' => $category112->id));
 392  
 393          $this->assertCount(0, $category2->get_all_children_ids());
 394          $this->assertCount(6, $category1->get_all_children_ids());
 395  
 396          $cmpchildrencat1 = array($category11->id, $category12->id, $category13->id, $category111->id, $category112->id,
 397                  $category1121->id);
 398          $childrencat1 = $category1->get_all_children_ids();
 399          // Order of values does not matter. Compare sorted arrays.
 400          sort($cmpchildrencat1);
 401          sort($childrencat1);
 402          $this->assertEquals($cmpchildrencat1, $childrencat1);
 403  
 404          $this->assertCount(3, $category11->get_all_children_ids());
 405          $this->assertCount(0, $category111->get_all_children_ids());
 406          $this->assertCount(1, $category112->get_all_children_ids());
 407  
 408          $this->assertEquals(array($category1121->id), $category112->get_all_children_ids());
 409      }
 410  
 411      /**
 412       * Test the countall function
 413       */
 414      public function test_count_all() {
 415          global $DB;
 416          // Dont assume there is just one. An add-on might create a category as part of the install.
 417          $numcategories = $DB->count_records('course_categories');
 418          $this->assertEquals($numcategories, core_course_category::count_all());
 419          $this->assertDebuggingCalled('Method core_course_category::count_all() is deprecated. Please use ' .
 420              'core_course_category::is_simple_site()', DEBUG_DEVELOPER);
 421          $category1 = core_course_category::create(array('name' => 'Cat1'));
 422          $category2 = core_course_category::create(array('name' => 'Cat2', 'parent' => $category1->id));
 423          $category3 = core_course_category::create(array('name' => 'Cat3', 'parent' => $category2->id, 'visible' => 0));
 424          // Now we've got three more.
 425          $this->assertEquals($numcategories + 3, core_course_category::count_all());
 426          $this->assertDebuggingCalled('Method core_course_category::count_all() is deprecated. Please use ' .
 427              'core_course_category::is_simple_site()', DEBUG_DEVELOPER);
 428          \cache_helper::purge_by_event('changesincoursecat');
 429          // We should still have 4.
 430          $this->assertEquals($numcategories + 3, core_course_category::count_all());
 431          $this->assertDebuggingCalled('Method core_course_category::count_all() is deprecated. Please use ' .
 432              'core_course_category::is_simple_site()', DEBUG_DEVELOPER);
 433      }
 434  
 435      /**
 436       * Test the is_simple_site function
 437       */
 438      public function test_is_simple_site() {
 439          // By default site has one category and is considered simple.
 440          $this->assertEquals(true, core_course_category::is_simple_site());
 441          $default = core_course_category::get_default();
 442          // When there is only one category but it is hidden, it is not a simple site.
 443          $default->update(['visible' => 0]);
 444          $this->assertEquals(false, core_course_category::is_simple_site());
 445          $default->update(['visible' => 1]);
 446          $this->assertEquals(true, core_course_category::is_simple_site());
 447          // As soon as there is more than one category, site is not simple any more.
 448          core_course_category::create(array('name' => 'Cat1'));
 449          $this->assertEquals(false, core_course_category::is_simple_site());
 450      }
 451  
 452      /**
 453       * Test a categories ability to resort courses.
 454       */
 455      public function test_resort_courses() {
 456          $this->resetAfterTest(true);
 457          $generator = $this->getDataGenerator();
 458          $category = $generator->create_category();
 459          $course1 = $generator->create_course(array(
 460              'category' => $category->id,
 461              'idnumber' => '006-01',
 462              'shortname' => 'Biome Study',
 463              'fullname' => '<span lang="ar" class="multilang">'.'دراسة منطقة إحيائية'.'</span><span lang="en" class="multilang">Biome Study</span>',
 464              'timecreated' => '1000000001'
 465          ));
 466          $course2 = $generator->create_course(array(
 467              'category' => $category->id,
 468              'idnumber' => '007-02',
 469              'shortname' => 'Chemistry Revision',
 470              'fullname' => 'Chemistry Revision',
 471              'timecreated' => '1000000002'
 472          ));
 473          $course3 = $generator->create_course(array(
 474              'category' => $category->id,
 475              'idnumber' => '007-03',
 476              'shortname' => 'Swiss Rolls and Sunflowers',
 477              'fullname' => 'Aarkvarks guide to Swiss Rolls and Sunflowers',
 478              'timecreated' => '1000000003'
 479          ));
 480          $course4 = $generator->create_course(array(
 481              'category' => $category->id,
 482              'idnumber' => '006-04',
 483              'shortname' => 'Scratch',
 484              'fullname' => '<a href="test.php">Basic Scratch</a>',
 485              'timecreated' => '1000000004'
 486          ));
 487          $c1 = (int)$course1->id;
 488          $c2 = (int)$course2->id;
 489          $c3 = (int)$course3->id;
 490          $c4 = (int)$course4->id;
 491  
 492          $coursecat = core_course_category::get($category->id);
 493          $this->assertTrue($coursecat->resort_courses('idnumber'));
 494          $this->assertSame(array($c1, $c4, $c2, $c3), array_keys($coursecat->get_courses()));
 495  
 496          $this->assertTrue($coursecat->resort_courses('shortname'));
 497          $this->assertSame(array($c1, $c2, $c4, $c3), array_keys($coursecat->get_courses()));
 498  
 499          $this->assertTrue($coursecat->resort_courses('timecreated'));
 500          $this->assertSame(array($c1, $c2, $c3, $c4), array_keys($coursecat->get_courses()));
 501  
 502          try {
 503              // Enable the multilang filter and set it to apply to headings and content.
 504              \filter_manager::reset_caches();
 505              filter_set_global_state('multilang', TEXTFILTER_ON);
 506              filter_set_applies_to_strings('multilang', true);
 507              $expected = array($c3, $c4, $c1, $c2);
 508          } catch (\coding_exception $ex) {
 509              $expected = array($c3, $c4, $c2, $c1);
 510          }
 511          $this->assertTrue($coursecat->resort_courses('fullname'));
 512          $this->assertSame($expected, array_keys($coursecat->get_courses()));
 513      }
 514  
 515      public function test_get_search_courses() {
 516          $cat1 = core_course_category::create(array('name' => 'Cat1'));
 517          $cat2 = core_course_category::create(array('name' => 'Cat2', 'parent' => $cat1->id));
 518          $c1 = $this->getDataGenerator()->create_course(array('category' => $cat1->id, 'fullname' => 'Test 3', 'summary' => ' ', 'idnumber' => 'ID3'));
 519          $c2 = $this->getDataGenerator()->create_course(array('category' => $cat1->id, 'fullname' => 'Test 1', 'summary' => ' ', 'visible' => 0));
 520          $c3 = $this->getDataGenerator()->create_course(array('category' => $cat1->id, 'fullname' => 'Математика', 'summary' => ' Test '));
 521          $c4 = $this->getDataGenerator()->create_course(array('category' => $cat1->id, 'fullname' => 'Test 4', 'summary' => ' ', 'idnumber' => 'ID4'));
 522  
 523          $c5 = $this->getDataGenerator()->create_course(array('category' => $cat2->id, 'fullname' => 'Test 5', 'summary' => ' '));
 524          $c6 = $this->getDataGenerator()->create_course(array('category' => $cat2->id, 'fullname' => 'Дискретная Математика', 'summary' => ' '));
 525          $c7 = $this->getDataGenerator()->create_course(array('category' => $cat2->id, 'fullname' => 'Test 7', 'summary' => ' ', 'visible' => 0));
 526          $c8 = $this->getDataGenerator()->create_course(array('category' => $cat2->id, 'fullname' => 'Test 8', 'summary' => ' '));
 527  
 528          // Get courses in category 1 (returned visible only because user is not enrolled).
 529          $res = $cat1->get_courses(array('sortorder' => 1));
 530          $this->assertEquals(array($c4->id, $c3->id, $c1->id), array_keys($res)); // Courses are added in reverse order.
 531          $this->assertEquals(3, $cat1->get_courses_count());
 532  
 533          // Get courses in category 1 recursively (returned visible only because user is not enrolled).
 534          $res = $cat1->get_courses(array('recursive' => 1));
 535          $this->assertEquals(array($c4->id, $c3->id, $c1->id, $c8->id, $c6->id, $c5->id), array_keys($res));
 536          $this->assertEquals(6, $cat1->get_courses_count(array('recursive' => 1)));
 537  
 538          // Get courses sorted by fullname.
 539          $res = $cat1->get_courses(array('sort' => array('fullname' => 1)));
 540          $this->assertEquals(array($c1->id, $c4->id, $c3->id), array_keys($res));
 541          $this->assertEquals(3, $cat1->get_courses_count(array('sort' => array('fullname' => 1))));
 542  
 543          // Get courses sorted by fullname recursively.
 544          $res = $cat1->get_courses(array('recursive' => 1, 'sort' => array('fullname' => 1)));
 545          $this->assertEquals(array($c1->id, $c4->id, $c5->id, $c8->id, $c6->id, $c3->id), array_keys($res));
 546          $this->assertEquals(6, $cat1->get_courses_count(array('recursive' => 1, 'sort' => array('fullname' => 1))));
 547  
 548          // Get courses sorted by fullname recursively, use offset and limit.
 549          $res = $cat1->get_courses(array('recursive' => 1, 'offset' => 1, 'limit' => 2, 'sort' => array('fullname' => -1)));
 550          $this->assertEquals(array($c6->id, $c8->id), array_keys($res));
 551          // Offset and limit do not affect get_courses_count().
 552          $this->assertEquals(6, $cat1->get_courses_count(array('recursive' => 1, 'offset' => 1, 'limit' => 2, 'sort' => array('fullname' => 1))));
 553  
 554          // Calling get_courses_count without prior call to get_courses().
 555          $this->assertEquals(3, $cat2->get_courses_count(array('recursive' => 1, 'sort' => array('idnumber' => 1))));
 556  
 557          // Search courses.
 558  
 559          // Search by text.
 560          $res = core_course_category::search_courses(array('search' => 'Test'));
 561          $this->assertEquals(array($c4->id, $c3->id, $c1->id, $c8->id, $c5->id), array_keys($res));
 562          $this->assertEquals(5, core_course_category::search_courses_count(array('search' => 'Test')));
 563  
 564          // Search by text with specified offset and limit.
 565          $options = array('sort' => array('fullname' => 1), 'offset' => 1, 'limit' => 2);
 566          $res = core_course_category::search_courses(array('search' => 'Test'), $options);
 567          $this->assertEquals(array($c4->id, $c5->id), array_keys($res));
 568          $this->assertEquals(5, core_course_category::search_courses_count(array('search' => 'Test'), $options));
 569  
 570          // IMPORTANT: the tests below may fail on some databases
 571          // case-insensitive search.
 572          $res = core_course_category::search_courses(array('search' => 'test'));
 573          $this->assertEquals(array($c4->id, $c3->id, $c1->id, $c8->id, $c5->id), array_keys($res));
 574          $this->assertEquals(5, core_course_category::search_courses_count(array('search' => 'test')));
 575  
 576          // Non-latin language search.
 577          $res = core_course_category::search_courses(array('search' => 'Математика'));
 578          $this->assertEquals(array($c3->id, $c6->id), array_keys($res));
 579          $this->assertEquals(2, core_course_category::search_courses_count(array('search' => 'Математика'), array()));
 580  
 581          $this->setUser($this->getDataGenerator()->create_user());
 582  
 583          // Add necessary capabilities.
 584          $this->assign_capability('moodle/course:create', CAP_ALLOW, \context_coursecat::instance($cat2->id));
 585          // Do another search with restricted capabilities.
 586          $reqcaps = array('moodle/course:create');
 587          $res = core_course_category::search_courses(array('search' => 'test'), array(), $reqcaps);
 588          $this->assertEquals(array($c8->id, $c5->id), array_keys($res));
 589          $this->assertEquals(2, core_course_category::search_courses_count(array('search' => 'test'), array(), $reqcaps));
 590      }
 591  
 592      public function test_course_contacts() {
 593          global $DB, $CFG;
 594  
 595          set_config('coursecontactduplicates', false);
 596  
 597          $teacherrole = $DB->get_record('role', array('shortname'=>'editingteacher'));
 598          $managerrole = $DB->get_record('role', array('shortname'=>'manager'));
 599          $studentrole = $DB->get_record('role', array('shortname'=>'student'));
 600          $oldcoursecontact = $CFG->coursecontact;
 601  
 602          $CFG->coursecontact = $managerrole->id. ','. $teacherrole->id;
 603  
 604          /*
 605           * User is listed in course contacts for the course if he has one of the
 606           * "course contact" roles ($CFG->coursecontact) AND is enrolled in the course.
 607           * If the user has several roles only the highest is displayed.
 608           */
 609  
 610          // Test case:
 611          //
 612          // == Cat1 (user2 has teacher role)
 613          //   == Cat2
 614          //     -- course21 (user2 is enrolled as manager) | [Expected] Manager: F2 L2
 615          //     -- course22 (user2 is enrolled as student) | [Expected] Teacher: F2 L2
 616          //     == Cat4 (user2 has manager role)
 617          //       -- course41 (user4 is enrolled as teacher, user5 is enrolled as manager) | [Expected] Manager: F5 L5, Teacher: F4 L4
 618          //       -- course42 (user2 is enrolled as teacher) | [Expected] Manager: F2 L2
 619          //   == Cat3 (user3 has manager role)
 620          //     -- course31 (user3 is enrolled as student) | [Expected] Manager: F3 L3
 621          //     -- course32                                | [Expected]
 622          //   -- course11 (user1 is enrolled as teacher)   | [Expected] Teacher: F1 L1
 623          //   -- course12 (user1 has teacher role)         | [Expected]
 624          //                also user4 is enrolled as teacher but enrolment is not active
 625          $category = $course = $enrol = $user = array();
 626          $category[1] = core_course_category::create(array('name' => 'Cat1'))->id;
 627          $category[2] = core_course_category::create(array('name' => 'Cat2', 'parent' => $category[1]))->id;
 628          $category[3] = core_course_category::create(array('name' => 'Cat3', 'parent' => $category[1]))->id;
 629          $category[4] = core_course_category::create(array('name' => 'Cat4', 'parent' => $category[2]))->id;
 630          foreach (array(1, 2, 3, 4) as $catid) {
 631              foreach (array(1, 2) as $courseid) {
 632                  $course[$catid][$courseid] = $this->getDataGenerator()->create_course(array('idnumber' => 'id'.$catid.$courseid,
 633                      'category' => $category[$catid]))->id;
 634                  $enrol[$catid][$courseid] = $DB->get_record('enrol', array('courseid'=>$course[$catid][$courseid], 'enrol'=>'manual'), '*', MUST_EXIST);
 635              }
 636          }
 637          foreach (array(1, 2, 3, 4, 5) as $userid) {
 638              $user[$userid] = $this->getDataGenerator()->create_user(array('firstname' => 'F'.$userid, 'lastname' => 'L'.$userid))->id;
 639          }
 640  
 641          $manual = enrol_get_plugin('manual');
 642  
 643          // Nobody is enrolled now and course contacts are empty.
 644          $allcourses = core_course_category::get(0)->get_courses(
 645              array('recursive' => true, 'coursecontacts' => true, 'sort' => array('idnumber' => 1)));
 646          foreach ($allcourses as $onecourse) {
 647              $this->assertEmpty($onecourse->get_course_contacts());
 648          }
 649  
 650          // Cat1 (user2 has teacher role)
 651          role_assign($teacherrole->id, $user[2], \context_coursecat::instance($category[1]));
 652          // course21 (user2 is enrolled as manager)
 653          $manual->enrol_user($enrol[2][1], $user[2], $managerrole->id);
 654          // course22 (user2 is enrolled as student)
 655          $manual->enrol_user($enrol[2][2], $user[2], $studentrole->id);
 656          // Cat4 (user2 has manager role)
 657          role_assign($managerrole->id, $user[2], \context_coursecat::instance($category[4]));
 658          // course41 (user4 is enrolled as teacher, user5 is enrolled as manager)
 659          $manual->enrol_user($enrol[4][1], $user[4], $teacherrole->id);
 660          $manual->enrol_user($enrol[4][1], $user[5], $managerrole->id);
 661          // course42 (user2 is enrolled as teacher)
 662          $manual->enrol_user($enrol[4][2], $user[2], $teacherrole->id);
 663          // Cat3 (user3 has manager role)
 664          role_assign($managerrole->id, $user[3], \context_coursecat::instance($category[3]));
 665          // course31 (user3 is enrolled as student)
 666          $manual->enrol_user($enrol[3][1], $user[3], $studentrole->id);
 667          // course11 (user1 is enrolled as teacher)
 668          $manual->enrol_user($enrol[1][1], $user[1], $teacherrole->id);
 669          // -- course12 (user1 has teacher role)
 670          //                also user4 is enrolled as teacher but enrolment is not active
 671          role_assign($teacherrole->id, $user[1], \context_course::instance($course[1][2]));
 672          $manual->enrol_user($enrol[1][2], $user[4], $teacherrole->id, 0, 0, ENROL_USER_SUSPENDED);
 673  
 674          $allcourses = core_course_category::get(0)->get_courses(
 675              array('recursive' => true, 'coursecontacts' => true, 'sort' => array('idnumber' => 1)));
 676          // Simplify the list of contacts for each course (similar as renderer would do).
 677          $contacts = array();
 678          foreach (array(1, 2, 3, 4) as $catid) {
 679              foreach (array(1, 2) as $courseid) {
 680                  $tmp = array();
 681                  foreach ($allcourses[$course[$catid][$courseid]]->get_course_contacts() as $contact) {
 682                      $tmp[] = $contact['rolename']. ': '. $contact['username'];
 683                  }
 684                  $contacts[$catid][$courseid] = join(', ', $tmp);
 685              }
 686          }
 687  
 688          // Assert:
 689          //     -- course21 (user2 is enrolled as manager) | Manager: F2 L2
 690          $this->assertSame('Manager: F2 L2', $contacts[2][1]);
 691          //     -- course22 (user2 is enrolled as student) | Teacher: F2 L2
 692          $this->assertSame('Teacher: F2 L2', $contacts[2][2]);
 693          //       -- course41 (user4 is enrolled as teacher, user5 is enrolled as manager) | Manager: F5 L5, Teacher: F4 L4
 694          $this->assertSame('Manager: F5 L5, Teacher: F4 L4', $contacts[4][1]);
 695          //       -- course42 (user2 is enrolled as teacher) | [Expected] Manager: F2 L2
 696          $this->assertSame('Manager: F2 L2', $contacts[4][2]);
 697          //     -- course31 (user3 is enrolled as student) | Manager: F3 L3
 698          $this->assertSame('Manager: F3 L3', $contacts[3][1]);
 699          //     -- course32                                |
 700          $this->assertSame('', $contacts[3][2]);
 701          //   -- course11 (user1 is enrolled as teacher)   | Teacher: F1 L1
 702          $this->assertSame('Teacher: F1 L1', $contacts[1][1]);
 703          //   -- course12 (user1 has teacher role)         |
 704          $this->assertSame('', $contacts[1][2]);
 705  
 706          // Suspend user 4 and make sure he is no longer in contacts of course 1 in category 4.
 707          $manual->enrol_user($enrol[4][1], $user[4], $teacherrole->id, 0, 0, ENROL_USER_SUSPENDED);
 708          $allcourses = core_course_category::get(0)->get_courses(array(
 709                  'recursive' => true,
 710                  'coursecontacts' => true,
 711                  'sort' => array('idnumber' => 1))
 712          );
 713          $contacts = $allcourses[$course[4][1]]->get_course_contacts();
 714          $this->assertCount(1, $contacts);
 715          $contact = reset($contacts);
 716          $this->assertEquals('F5 L5', $contact['username']);
 717  
 718          $CFG->coursecontact = $oldcoursecontact;
 719      }
 720  
 721      public function test_course_contacts_with_duplicates() {
 722          global $DB, $CFG;
 723  
 724          set_config('coursecontactduplicates', true);
 725  
 726          $displayall = get_config('core', 'coursecontactduplicates');
 727          $this->assertEquals(true, $displayall);
 728  
 729          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
 730          $managerrole = $DB->get_record('role', array('shortname' => 'manager'));
 731          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 732          $oldcoursecontact = $CFG->coursecontact;
 733  
 734          $CFG->coursecontact = $managerrole->id. ','. $teacherrole->id;
 735  
 736          /*
 737          * User is listed in course contacts for the course if he has one of the
 738          * "course contact" roles ($CFG->coursecontact) AND is enrolled in the course.
 739          * If the user has several roles all roles are displayed, but each role only once per user.
 740          */
 741  
 742          /*
 743          * Test case:
 744          *
 745          * == Cat1 (user2 has teacher role)
 746          *    == Cat2
 747          *      -- course21 (user2 is enrolled as manager) | [Expected] Manager: F2 L2
 748          *      -- course22 (user2 is enrolled as student) | [Expected] Teacher: F2 L2
 749          *      == Cat4 (user2 has manager role)
 750          *        -- course41 (user4 is enrolled as teacher, user5 is enrolled as manager)
 751          *                                                 | [Expected] Manager: F5 L5, Teacher: F4 L4
 752          *        -- course42 (user2 is enrolled as teacher) | [Expected] Manager: F2 L2
 753          *    == Cat3 (user3 has manager role)
 754          *      -- course31 (user3 is enrolled as student) | [Expected] Manager: F3 L3
 755          *      -- course32                                | [Expected]
 756          *    -- course11 (user1 is enrolled as teacher)   | [Expected] Teacher: F1 L1
 757          *    -- course12 (user1 has teacher role)         | [Expected]
 758          *                 also user4 is enrolled as teacher but enrolment is not active
 759          */
 760          $category = $course = $enrol = $user = array();
 761          $category[1] = core_course_category::create(array('name' => 'Cat1'))->id;
 762          $category[2] = core_course_category::create(array('name' => 'Cat2', 'parent' => $category[1]))->id;
 763          $category[3] = core_course_category::create(array('name' => 'Cat3', 'parent' => $category[1]))->id;
 764          $category[4] = core_course_category::create(array('name' => 'Cat4', 'parent' => $category[2]))->id;
 765          foreach (array(1, 2, 3, 4) as $catid) {
 766              foreach (array(1, 2) as $courseid) {
 767                  $course[$catid][$courseid] = $this->getDataGenerator()->create_course(array(
 768                          'idnumber' => 'id'.$catid.$courseid,
 769                          'category' => $category[$catid])
 770                  )->id;
 771                  $enrol[$catid][$courseid] = $DB->get_record(
 772                          'enrol',
 773                          array('courseid' => $course[$catid][$courseid], 'enrol' => 'manual'),
 774                          '*',
 775                          MUST_EXIST
 776                  );
 777              }
 778          }
 779          foreach (array(1, 2, 3, 4, 5) as $userid) {
 780              $user[$userid] = $this->getDataGenerator()->create_user(array(
 781                              'firstname' => 'F'.$userid,
 782                              'lastname' => 'L'.$userid)
 783              )->id;
 784          }
 785  
 786          $manual = enrol_get_plugin('manual');
 787  
 788          // Nobody is enrolled now and course contacts are empty.
 789          $allcourses = core_course_category::get(0)->get_courses(array(
 790                  'recursive' => true,
 791                  'coursecontacts' => true,
 792                  'sort' => array('idnumber' => 1))
 793          );
 794          foreach ($allcourses as $onecourse) {
 795              $this->assertEmpty($onecourse->get_course_contacts());
 796          }
 797  
 798          // Cat1: user2 has teacher role.
 799          role_assign($teacherrole->id, $user[2], \context_coursecat::instance($category[1]));
 800          // Course21: user2 is enrolled as manager.
 801          $manual->enrol_user($enrol[2][1], $user[2], $managerrole->id);
 802          // Course22: user2 is enrolled as student.
 803          $manual->enrol_user($enrol[2][2], $user[2], $studentrole->id);
 804          // Cat4: user2 has manager role.
 805          role_assign($managerrole->id, $user[2], \context_coursecat::instance($category[4]));
 806          // Course41: user4 is enrolled as teacher, user5 is enrolled as manager.
 807          $manual->enrol_user($enrol[4][1], $user[4], $teacherrole->id);
 808          $manual->enrol_user($enrol[4][1], $user[5], $managerrole->id);
 809          // Course42: user2 is enrolled as teacher.
 810          $manual->enrol_user($enrol[4][2], $user[2], $teacherrole->id);
 811          // Cat3: user3 has manager role.
 812          role_assign($managerrole->id, $user[3], \context_coursecat::instance($category[3]));
 813          // Course31: user3 is enrolled as student.
 814          $manual->enrol_user($enrol[3][1], $user[3], $studentrole->id);
 815          // Course11: user1 is enrolled as teacher and user4 is enrolled as teacher and has manager role.
 816          $manual->enrol_user($enrol[1][1], $user[1], $teacherrole->id);
 817          $manual->enrol_user($enrol[1][1], $user[4], $teacherrole->id);
 818          role_assign($managerrole->id, $user[4], \context_course::instance($course[1][1]));
 819          // Course12: user1 has teacher role, but is not enrolled, as well as user4 is enrolled as teacher, but user4's enrolment is
 820          // not active.
 821          role_assign($teacherrole->id, $user[1], \context_course::instance($course[1][2]));
 822          $manual->enrol_user($enrol[1][2], $user[4], $teacherrole->id, 0, 0, ENROL_USER_SUSPENDED);
 823  
 824          $allcourses = core_course_category::get(0)->get_courses(
 825                  array('recursive' => true, 'coursecontacts' => true, 'sort' => array('idnumber' => 1)));
 826          // Simplify the list of contacts for each course (similar as renderer would do).
 827          $contacts = array();
 828          foreach (array(1, 2, 3, 4) as $catid) {
 829              foreach (array(1, 2) as $courseid) {
 830                  $tmp = array();
 831                  foreach ($allcourses[$course[$catid][$courseid]]->get_course_contacts() as $contact) {
 832                      $rolenames = array_map(function ($role) {
 833                          return $role->displayname;
 834                      }, $contact['roles']);
 835                      $tmp[] = implode(", ", $rolenames). ': '.
 836                              $contact['username'];
 837                  }
 838                  $contacts[$catid][$courseid] = join(', ', $tmp);
 839              }
 840          }
 841  
 842          // Assert:
 843          // Course21: user2 is enrolled as manager. [Expected] Manager: F2 L2, Teacher: F2 L2.
 844          $this->assertSame('Manager, Teacher: F2 L2', $contacts[2][1]);
 845          // Course22: user2 is enrolled as student. [Expected] Teacher: F2 L2.
 846          $this->assertSame('Teacher: F2 L2', $contacts[2][2]);
 847          // Course41: user4 is enrolled as teacher, user5 is enrolled as manager. [Expected] Manager: F5 L5, Teacher: F4 L4.
 848          $this->assertSame('Manager: F5 L5, Teacher: F4 L4', $contacts[4][1]);
 849          // Course42: user2 is enrolled as teacher. [Expected] Manager: F2 L2, Teacher: F2 L2.
 850          $this->assertSame('Manager, Teacher: F2 L2', $contacts[4][2]);
 851          // Course31: user3 is enrolled as student. [Expected] Manager: F3 L3.
 852          $this->assertSame('Manager: F3 L3', $contacts[3][1]);
 853          // Course32: nobody is enrolled. [Expected] (nothing).
 854          $this->assertSame('', $contacts[3][2]);
 855          // Course11: user1 is enrolled as teacher and user4 is enrolled as teacher and has manager role. [Expected] Manager: F4 L4,
 856          // Teacher: F1 L1, Teacher: F4 L4.
 857          $this->assertSame('Manager, Teacher: F4 L4, Teacher: F1 L1', $contacts[1][1]);
 858          // Course12: user1 has teacher role, but is not enrolled, as well as user4 is enrolled as teacher, but user4's enrolment is
 859          // not active. [Expected] (nothing).
 860          $this->assertSame('', $contacts[1][2]);
 861  
 862          // Suspend user 4 and make sure he is no longer in contacts of course 1 in category 4.
 863          $manual->enrol_user($enrol[4][1], $user[4], $teacherrole->id, 0, 0, ENROL_USER_SUSPENDED);
 864          $allcourses = core_course_category::get(0)->get_courses(array(
 865                  'recursive' => true,
 866                  'coursecontacts' => true,
 867                  'sort' => array('idnumber' => 1)
 868          ));
 869          $contacts = $allcourses[$course[4][1]]->get_course_contacts();
 870          $this->assertCount(1, $contacts);
 871          $contact = reset($contacts);
 872          $this->assertEquals('F5 L5', $contact['username']);
 873  
 874          $CFG->coursecontact = $oldcoursecontact;
 875      }
 876  
 877      public function test_overview_files() {
 878          global $CFG;
 879          $this->setAdminUser();
 880          $cat1 = core_course_category::create(array('name' => 'Cat1'));
 881  
 882          // Create course c1 with one image file.
 883          $dratid1 = $this->fill_draft_area(array('filename.jpg' => 'Test file contents1'));
 884          $c1 = $this->getDataGenerator()->create_course(array('category' => $cat1->id,
 885              'fullname' => 'Test 1', 'overviewfiles_filemanager' => $dratid1));
 886          // Create course c2 with two image files (only one file will be added because of settings).
 887          $dratid2 = $this->fill_draft_area(array('filename21.jpg' => 'Test file contents21', 'filename22.jpg' => 'Test file contents22'));
 888          $c2 = $this->getDataGenerator()->create_course(array('category' => $cat1->id,
 889              'fullname' => 'Test 2', 'overviewfiles_filemanager' => $dratid2));
 890          // Create course c3 without files.
 891          $c3 = $this->getDataGenerator()->create_course(array('category' => $cat1->id, 'fullname' => 'Test 3'));
 892  
 893          // Change the settings to allow multiple files of any types.
 894          $CFG->courseoverviewfileslimit = 3;
 895          $CFG->courseoverviewfilesext = '*';
 896          // Create course c5 with two image files.
 897          $dratid4 = $this->fill_draft_area(array('filename41.jpg' => 'Test file contents41', 'filename42.jpg' => 'Test file contents42'));
 898          $c4 = $this->getDataGenerator()->create_course(array('category' => $cat1->id,
 899              'fullname' => 'Test 4', 'overviewfiles_filemanager' => $dratid4));
 900          // Create course c6 with non-image file.
 901          $dratid5 = $this->fill_draft_area(array('filename51.zip' => 'Test file contents51'));
 902          $c5 = $this->getDataGenerator()->create_course(array('category' => $cat1->id,
 903              'fullname' => 'Test 5', 'overviewfiles_filemanager' => $dratid5));
 904  
 905          // Reset default settings.
 906          $CFG->courseoverviewfileslimit = 1;
 907          $CFG->courseoverviewfilesext = 'web_image';
 908  
 909          $courses = $cat1->get_courses();
 910          $this->assertTrue($courses[$c1->id]->has_course_overviewfiles());
 911          $this->assertTrue($courses[$c2->id]->has_course_overviewfiles());
 912          $this->assertFalse($courses[$c3->id]->has_course_overviewfiles());
 913          $this->assertTrue($courses[$c4->id]->has_course_overviewfiles());
 914          $this->assertTrue($courses[$c5->id]->has_course_overviewfiles()); // Does not validate the filetypes.
 915  
 916          $this->assertEquals(1, count($courses[$c1->id]->get_course_overviewfiles()));
 917          $this->assertEquals(1, count($courses[$c2->id]->get_course_overviewfiles()));
 918          $this->assertEquals(0, count($courses[$c3->id]->get_course_overviewfiles()));
 919          $this->assertEquals(1, count($courses[$c4->id]->get_course_overviewfiles()));
 920          $this->assertEquals(0, count($courses[$c5->id]->get_course_overviewfiles())); // Validate the filetypes.
 921  
 922          // Overview files are not allowed, all functions return empty values.
 923          $CFG->courseoverviewfileslimit = 0;
 924  
 925          $this->assertFalse($courses[$c1->id]->has_course_overviewfiles());
 926          $this->assertFalse($courses[$c2->id]->has_course_overviewfiles());
 927          $this->assertFalse($courses[$c3->id]->has_course_overviewfiles());
 928          $this->assertFalse($courses[$c4->id]->has_course_overviewfiles());
 929          $this->assertFalse($courses[$c5->id]->has_course_overviewfiles());
 930  
 931          $this->assertEquals(0, count($courses[$c1->id]->get_course_overviewfiles()));
 932          $this->assertEquals(0, count($courses[$c2->id]->get_course_overviewfiles()));
 933          $this->assertEquals(0, count($courses[$c3->id]->get_course_overviewfiles()));
 934          $this->assertEquals(0, count($courses[$c4->id]->get_course_overviewfiles()));
 935          $this->assertEquals(0, count($courses[$c5->id]->get_course_overviewfiles()));
 936  
 937          // Multiple overview files are allowed but still limited to images.
 938          $CFG->courseoverviewfileslimit = 3;
 939  
 940          $this->assertTrue($courses[$c1->id]->has_course_overviewfiles());
 941          $this->assertTrue($courses[$c2->id]->has_course_overviewfiles());
 942          $this->assertFalse($courses[$c3->id]->has_course_overviewfiles());
 943          $this->assertTrue($courses[$c4->id]->has_course_overviewfiles());
 944          $this->assertTrue($courses[$c5->id]->has_course_overviewfiles()); // Still does not validate the filetypes.
 945  
 946          $this->assertEquals(1, count($courses[$c1->id]->get_course_overviewfiles()));
 947          $this->assertEquals(1, count($courses[$c2->id]->get_course_overviewfiles())); // Only 1 file was actually added.
 948          $this->assertEquals(0, count($courses[$c3->id]->get_course_overviewfiles()));
 949          $this->assertEquals(2, count($courses[$c4->id]->get_course_overviewfiles()));
 950          $this->assertEquals(0, count($courses[$c5->id]->get_course_overviewfiles()));
 951  
 952          // Multiple overview files of any type are allowed.
 953          $CFG->courseoverviewfilesext = '*';
 954  
 955          $this->assertTrue($courses[$c1->id]->has_course_overviewfiles());
 956          $this->assertTrue($courses[$c2->id]->has_course_overviewfiles());
 957          $this->assertFalse($courses[$c3->id]->has_course_overviewfiles());
 958          $this->assertTrue($courses[$c4->id]->has_course_overviewfiles());
 959          $this->assertTrue($courses[$c5->id]->has_course_overviewfiles());
 960  
 961          $this->assertEquals(1, count($courses[$c1->id]->get_course_overviewfiles()));
 962          $this->assertEquals(1, count($courses[$c2->id]->get_course_overviewfiles()));
 963          $this->assertEquals(0, count($courses[$c3->id]->get_course_overviewfiles()));
 964          $this->assertEquals(2, count($courses[$c4->id]->get_course_overviewfiles()));
 965          $this->assertEquals(1, count($courses[$c5->id]->get_course_overviewfiles()));
 966      }
 967  
 968      public function test_get_nested_name() {
 969          $cat1name = 'Cat1';
 970          $cat2name = 'Cat2';
 971          $cat3name = 'Cat3';
 972          $cat4name = 'Cat4';
 973          $category1 = core_course_category::create(array('name' => $cat1name));
 974          $category2 = core_course_category::create(array('name' => $cat2name, 'parent' => $category1->id));
 975          $category3 = core_course_category::create(array('name' => $cat3name, 'parent' => $category2->id));
 976          $category4 = core_course_category::create(array('name' => $cat4name, 'parent' => $category2->id));
 977  
 978          $this->assertEquals($cat1name, $category1->get_nested_name(false));
 979          $this->assertEquals("{$cat1name} / {$cat2name}", $category2->get_nested_name(false));
 980          $this->assertEquals("{$cat1name} / {$cat2name} / {$cat3name}", $category3->get_nested_name(false));
 981          $this->assertEquals("{$cat1name} / {$cat2name} / {$cat4name}", $category4->get_nested_name(false));
 982      }
 983  
 984      public function test_coursecat_is_uservisible() {
 985          global $USER;
 986  
 987          // Create category 1 as visible.
 988          $category1 = core_course_category::create(array('name' => 'Cat1', 'visible' => 1));
 989          // Create category 2 as hidden.
 990          $category2 = core_course_category::create(array('name' => 'Cat2', 'visible' => 0));
 991  
 992          $this->assertTrue($category1->is_uservisible());
 993          $this->assertFalse($category2->is_uservisible());
 994  
 995          $this->assign_capability('moodle/category:viewhiddencategories');
 996  
 997          $this->assertTrue($category1->is_uservisible());
 998          $this->assertTrue($category2->is_uservisible());
 999  
1000          // First, store current user's id, then login as another user.
1001          $userid = $USER->id;
1002          $this->setUser($this->getDataGenerator()->create_user());
1003  
1004          // User $user should still have the moodle/category:viewhiddencategories capability.
1005          $this->assertTrue($category1->is_uservisible($userid));
1006          $this->assertTrue($category2->is_uservisible($userid));
1007  
1008          $this->assign_capability('moodle/category:viewhiddencategories', CAP_INHERIT);
1009  
1010          $this->assertTrue($category1->is_uservisible());
1011          $this->assertFalse($category2->is_uservisible());
1012      }
1013  
1014      public function test_current_user_coursecat_get() {
1015          $this->assign_capability('moodle/category:viewhiddencategories');
1016  
1017          // Create category 1 as visible.
1018          $category1 = core_course_category::create(array('name' => 'Cat1', 'visible' => 1));
1019          // Create category 2 as hidden.
1020          $category2 = core_course_category::create(array('name' => 'Cat2', 'visible' => 0));
1021  
1022          $this->assertEquals($category1->id, core_course_category::get($category1->id)->id);
1023          $this->assertEquals($category2->id, core_course_category::get($category2->id)->id);
1024  
1025          // Login as another user to test core_course_category::get.
1026          $this->setUser($this->getDataGenerator()->create_user());
1027          $this->assertEquals($category1->id, core_course_category::get($category1->id)->id);
1028  
1029          // Expecting to get an exception as this new user does not have the moodle/category:viewhiddencategories capability.
1030          $this->expectException('moodle_exception');
1031          $this->expectExceptionMessage(get_string('cannotviewcategory', 'error'));
1032          core_course_category::get($category2->id);
1033      }
1034  
1035      public function test_another_user_coursecat_get() {
1036          global $USER;
1037  
1038          $this->assign_capability('moodle/category:viewhiddencategories');
1039  
1040          // Create category 1 as visible.
1041          $category1 = core_course_category::create(array('name' => 'Cat1', 'visible' => 1));
1042          // Create category 2 as hidden.
1043          $category2 = core_course_category::create(array('name' => 'Cat2', 'visible' => 0));
1044  
1045          // First, store current user's object, then login as another user.
1046          $user1 = $USER;
1047          $user2 = $this->getDataGenerator()->create_user();
1048          $this->setUser($user2);
1049  
1050          $this->assertEquals($category1->id, core_course_category::get($category1->id, MUST_EXIST, false, $user1)->id);
1051          $this->assertEquals($category2->id, core_course_category::get($category2->id, MUST_EXIST, false, $user1)->id);
1052  
1053          $this->setUser($user1);
1054  
1055          $this->assertEquals($category1->id, core_course_category::get($category1->id, MUST_EXIST, false, $user2)->id);
1056          $this->expectException('moodle_exception');
1057          $this->expectExceptionMessage(get_string('cannotviewcategory', 'error'));
1058          core_course_category::get($category2->id, MUST_EXIST, false, $user2);
1059      }
1060  
1061      /**
1062       * Creates a draft area for current user and fills it with fake files
1063       *
1064       * @param array $files array of files that need to be added to filearea, filename => filecontents
1065       * @return int draftid for the filearea
1066       */
1067      protected function fill_draft_area(array $files) {
1068          global $USER;
1069          $usercontext = \context_user::instance($USER->id);
1070          $draftid = file_get_unused_draft_itemid();
1071          foreach ($files as $filename => $filecontents) {
1072              // Add actual file there.
1073              $filerecord = array('component' => 'user', 'filearea' => 'draft',
1074                      'contextid' => $usercontext->id, 'itemid' => $draftid,
1075                      'filename' => $filename, 'filepath' => '/');
1076              $fs = get_file_storage();
1077              $fs->create_file_from_string($filerecord, $filecontents);
1078          }
1079          return $draftid;
1080      }
1081  
1082      /**
1083       * This test ensures that is the list of courses in a category can be retrieved while a course is being deleted.
1084       */
1085      public function test_get_courses_during_delete() {
1086          global $DB;
1087          $category = self::getDataGenerator()->create_category();
1088          $course = self::getDataGenerator()->create_course(['category' => $category->id]);
1089          $othercourse = self::getDataGenerator()->create_course(['category' => $category->id]);
1090          $coursecategory = core_course_category::get($category->id);
1091          // Get a list of courses before deletion to populate the cache.
1092          $originalcourses = $coursecategory->get_courses();
1093          $this->assertCount(2, $originalcourses);
1094          $this->assertArrayHasKey($course->id, $originalcourses);
1095          $this->assertArrayHasKey($othercourse->id, $originalcourses);
1096          // Simulate the course deletion process being part way though.
1097          $DB->delete_records('course', ['id' => $course->id]);
1098          // Get the list of courses while a deletion is in progress.
1099          $courses = $coursecategory->get_courses();
1100          $this->assertCount(1, $courses);
1101          $this->assertArrayHasKey($othercourse->id, $courses);
1102      }
1103  
1104      /**
1105       * Test get_nearest_editable_subcategory() method.
1106       *
1107       * @covers \core_course_category::get_nearest_editable_subcategory
1108       */
1109      public function test_get_nearest_editable_subcategory(): void {
1110          global $DB;
1111  
1112          $coursecreatorrole = $DB->get_record('role', ['shortname' => 'coursecreator']);
1113          $managerrole = $DB->get_record('role', ['shortname' => 'manager']);
1114  
1115          // Create categories.
1116          $category1 = core_course_category::create(['name' => 'Cat1']);
1117          $category2 = core_course_category::create(['name' => 'Cat2']);
1118          $category3 = core_course_category::create(['name' => 'Cat3']);
1119          // Get the category contexts.
1120          $category1context = $category1->get_context();
1121          $category2context = $category2->get_context();
1122          $category3context = $category3->get_context();
1123          // Create user.
1124          $user1 = $this->getDataGenerator()->create_user();
1125          $user2 = $this->getDataGenerator()->create_user();
1126          $user3 = $this->getDataGenerator()->create_user();
1127          // Assign the user1 to 'Course creator' role for Cat1.
1128          role_assign($coursecreatorrole->id, $user1->id, $category1context->id);
1129          // Assign the user2 to 'Manager' role for Cat3.
1130          role_assign($managerrole->id, $user2->id, $category3context->id);
1131  
1132          // Start scenario 1.
1133          // user3 has no permission to create course or manage category.
1134          $this->setUser($user3);
1135          $coursecat = core_course_category::user_top();
1136          $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create']));
1137          $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create']));
1138          $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['manage']));
1139          $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage']));
1140          $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage']));
1141          // End scenario 1.
1142  
1143          // Start scenario 2.
1144          // user1 has permission to create course but has no permission to manage category.
1145          $this->setUser($user1);
1146          $coursecat = core_course_category::user_top();
1147          $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create']));
1148          $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create']));
1149          $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['manage']));
1150          $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage']));
1151          $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage']));
1152          // The get_nearest_editable_subcategory should return Cat1.
1153          $this->assertEquals($category1->id, core_course_category::get_nearest_editable_subcategory($coursecat, ['create'])->id);
1154          $this->assertEquals($category1->id,
1155              core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create'])->id);
1156          // Assign the user1 to 'Course creator' role for Cat2.
1157          role_assign($coursecreatorrole->id, $user1->id, $category2context->id);
1158          // The get_nearest_editable_subcategory should still return Cat1 (First creatable subcategory) for create course capability.
1159          $this->assertEquals($category1->id, core_course_category::get_nearest_editable_subcategory($coursecat, ['create'])->id);
1160          $this->assertEquals($category1->id,
1161              core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create'])->id);
1162          // End scenario 2.
1163  
1164          // Start scenario 3.
1165          // user2 has no permission to create course but has permission to manage category.
1166          $this->setUser($user2);
1167          // Remove the moodle/course:create capability for the manager role.
1168          unassign_capability('moodle/course:create', $managerrole->id);
1169          $coursecat = core_course_category::user_top();
1170          $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create']));
1171          $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create']));
1172          $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['manage']));
1173          $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage']));
1174          $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage']));
1175          // The get_nearest_editable_subcategory should return Cat3.
1176          $this->assertEquals($category3->id, core_course_category::get_nearest_editable_subcategory($coursecat, ['manage'])->id);
1177          $this->assertEquals($category3->id,
1178              core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage'])->id);
1179          // End scenario 3.
1180  
1181          // Start scenario 4.
1182          // user2 has both permission to create course and manage category.
1183          // Add the moodle/course:create capability back again for the manager role.
1184          assign_capability('moodle/course:create', CAP_ALLOW, $managerrole->id, $category3context->id);
1185          $this->setUser($user2);
1186          $coursecat = core_course_category::user_top();
1187          $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create']));
1188          $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create']));
1189          $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['manage']));
1190          $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage']));
1191          $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage']));
1192          // The get_nearest_editable_subcategory should return Cat3.
1193          $this->assertEquals($category3->id,
1194              core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage'])->id);
1195          $this->assertEquals($category3->id, core_course_category::get_nearest_editable_subcategory($coursecat,
1196              ['moodle/course:create', 'moodle/category:manage'])->id);
1197          // End scenario 4.
1198  
1199          // Start scenario 5.
1200          // Exception will be thrown if $permissionstocheck is empty.
1201          $this->setUser($user1);
1202          $coursecat = core_course_category::user_top();
1203          $this->expectException('coding_exception');
1204          $this->expectExceptionMessage('Invalid permissionstocheck parameter');
1205          $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, []));
1206          // End scenario 5.
1207      }
1208  
1209      /**
1210       * Test get_nearest_editable_subcategory() method with hidden categories.
1211       *
1212       * @param int $visible  Whether the category is visible or not.
1213       * @param bool $child   Whether the category is child of main category or not.
1214       * @param string $role  The role the user must have.
1215       * @param array $permissions An array of permissions we must check.
1216       * @param bool $result Whether the result should be the category or null.
1217       *
1218       * @dataProvider get_nearest_editable_subcategory_provider
1219       * @covers \core_course_category::get_nearest_editable_subcategory
1220       */
1221      public function test_get_nearest_editable_subcategory_with_hidden_categories(
1222          int $visible = 0,
1223          bool $child = false,
1224          string $role = 'manager',
1225          array $permissions = [],
1226          bool $result = false
1227      ): void {
1228          global $DB;
1229  
1230          $userrole = $DB->get_record('role', ['shortname' => $role]);
1231          $maincat = core_course_category::create(['name' => 'Main cat']);
1232  
1233          $catparams = new \stdClass();
1234          $catparams->name = 'Test category';
1235          $catparams->visible = $visible;
1236          if ($child) {
1237              $catparams->parent = $maincat->id;
1238          }
1239          $category = core_course_category::create($catparams);
1240          $catcontext = $category->get_context();
1241          $user = $this->getDataGenerator()->create_user();
1242          role_assign($userrole->id, $user->id, $catcontext->id);
1243          $this->setUser($user);
1244  
1245          $nearestcat = core_course_category::get_nearest_editable_subcategory(core_course_category::user_top(), $permissions);
1246  
1247          if ($result) {
1248              $this->assertEquals($category->id, $nearestcat->id);
1249          } else {
1250              $this->assertEmpty($nearestcat);
1251          }
1252      }
1253  
1254      /**
1255       * Data provider for test_get_nearest_editable_subcategory_with_hidden_categories().
1256       *
1257       * @return array
1258       */
1259      public function get_nearest_editable_subcategory_provider(): array {
1260          return [
1261              'Hidden main category for manager. Checking create and manage' => [
1262                  0,
1263                  false,
1264                  'manager',
1265                  ['create', 'manage'],
1266                  true,
1267              ],
1268              'Hidden main category for course creator. Checking create and manage' => [
1269                  0,
1270                  false,
1271                  'coursecreator',
1272                  ['create', 'manage'],
1273                  false,
1274              ],
1275              'Hidden main category for student. Checking create and manage' => [
1276                  0,
1277                  false,
1278                  'student',
1279                  ['create', 'manage'],
1280                  false,
1281              ],
1282              'Hidden main category for manager. Checking create' => [
1283                  0,
1284                  false,
1285                  'manager',
1286                  ['create'],
1287                  true,
1288              ],
1289              'Hidden main category for course creator. Checking create' => [
1290                  0,
1291                  false,
1292                  'coursecreator',
1293                  ['create'],
1294                  true,
1295              ],
1296              'Hidden main category for student. Checking create' => [
1297                  0,
1298                  false,
1299                  'student',
1300                  ['create'],
1301                  false,
1302              ],
1303              'Hidden subcategory for manager. Checking create and manage' => [
1304                  0,
1305                  true,
1306                  'manager',
1307                  ['create', 'manage'],
1308                  true,
1309              ],
1310              'Hidden subcategory for course creator. Checking create and manage' => [
1311                  0,
1312                  true,
1313                  'coursecreator',
1314                  ['create', 'manage'],
1315                  false,
1316              ],
1317              'Hidden subcategory for student. Checking create and manage' => [
1318                  0,
1319                  true,
1320                  'student',
1321                  ['create', 'manage'],
1322                  false,
1323              ],
1324              'Hidden subcategory for manager. Checking create' => [
1325                  0,
1326                  true,
1327                  'manager',
1328                  ['create'],
1329                  true,
1330              ],
1331              'Hidden subcategory for course creator. Checking create' => [
1332                  0,
1333                  true,
1334                  'coursecreator',
1335                  ['create'],
1336                  true,
1337              ],
1338              'Hidden subcategory for student. Checking create' => [
1339                  0,
1340                  true,
1341                  'student',
1342                  ['create'],
1343                  false,
1344              ],
1345          ];
1346      }
1347  
1348      /**
1349       * This test ensures that the filter context list is populated by the correct filter contexts from make_category_list.
1350       *
1351       * @coversNothing
1352       */
1353      public function test_make_category_list_context() {
1354          global $DB;
1355          // Ensure that the category list is empty.
1356          $DB->delete_records('course_categories');
1357          set_config('perfdebug', 15);
1358  
1359          // Create a few categories to populate the context cache.
1360          $this->getDataGenerator()->create_category(['name' => 'cat1']);
1361          $this->getDataGenerator()->create_category(['name' => 'cat2']);
1362          $this->getDataGenerator()->create_category(['name' => 'cat3']);
1363          $filtermanager = \filter_manager::instance();
1364  
1365          // Configure a filter to apply to all content and headings.
1366          filter_set_global_state('multilang', TEXTFILTER_ON);
1367          filter_set_applies_to_strings('multilang', true);
1368  
1369          $perf = $filtermanager->get_performance_summary();
1370          $this->assertEquals(0, $perf[0]['contextswithfilters']);
1371  
1372          // Now fill the cache with the category strings.
1373          \core_course_category::make_categories_list();
1374          // 3 Categories + system context.
1375          $perf = $filtermanager->get_performance_summary();
1376          $this->assertEquals(3, $perf[0]['contextswithfilters']);
1377          $filtermanager->reset_caches();
1378          // We need to refresh the instance, resetting caches unloads the singleton.
1379          $filtermanager = \filter_manager::instance();
1380          \cache_helper::purge_by_definition('core', 'coursecat');
1381  
1382          // Now flip the bit on the filter context.
1383          set_config('filternavigationwithsystemcontext', 1);
1384  
1385          // Repeat the check. Only context should be system context.
1386          \core_course_category::make_categories_list();
1387          $perf = $filtermanager->get_performance_summary();
1388          $this->assertEquals(1, $perf[0]['contextswithfilters']);
1389      }
1390  }