on-demand release 4.5dev+
[moodle.git] / course / tests / category_test.php
blobb6eba967d4ba0df3e74b1ac03cd1ec6fd5642c2e
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.
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/>.
17 namespace core_course;
19 use core_course_category;
21 /**
22 * Tests for class core_course_category
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
29 class category_test extends \advanced_testcase {
31 protected $roles;
33 protected function setUp(): void {
34 parent::setUp();
35 $this->resetAfterTest();
36 $user = $this->getDataGenerator()->create_user();
37 $this->setUser($user);
40 protected function get_roleid($context = null) {
41 global $USER;
42 if ($context === null) {
43 $context = \context_system::instance();
45 if (is_object($context)) {
46 $context = $context->id;
48 if (empty($this->roles)) {
49 $this->roles = array();
51 if (empty($this->roles[$USER->id])) {
52 $this->roles[$USER->id] = array();
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);
58 return $this->roles[$USER->id][$context];
61 protected function assign_capability($capability, $permission = CAP_ALLOW, $contextid = null) {
62 if ($contextid === null) {
63 $contextid = \context_system::instance();
65 if (is_object($contextid)) {
66 $contextid = $contextid->id;
68 assign_capability($capability, $permission, $this->get_roleid($contextid), $contextid, true);
69 accesslib_clear_all_caches_for_unit_testing();
72 public function test_create_coursecat(): void {
73 // Create the category.
74 $data = new \stdClass();
75 $data->name = 'aaa';
76 $data->description = 'aaa';
77 $data->idnumber = '';
79 $category1 = core_course_category::create($data);
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);
86 $this->assertGreaterThanOrEqual(1, $category1->sortorder);
88 // Create two more categories and test the sortorder worked correctly.
89 $data->name = 'ccc';
90 $category2 = core_course_category::create($data);
92 $data->name = 'bbb';
93 $category3 = core_course_category::create($data);
95 $this->assertGreaterThan($category1->sortorder, $category2->sortorder);
96 $this->assertGreaterThan($category2->sortorder, $category3->sortorder);
99 public function test_name_idnumber_exceptions(): void {
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);
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);
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);
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);
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);
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);
143 public function test_visibility(): void {
144 $this->assign_capability('moodle/category:viewhiddencategories');
145 $this->assign_capability('moodle/category:manage');
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
211 public function test_hierarchy(): void {
212 $this->assign_capability('moodle/category:viewhiddencategories');
213 $this->assign_capability('moodle/category:manage');
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));
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());
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));
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);
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()));
244 public function test_update(): void {
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);
259 $this->assertGreaterThan($timecreated, $category1->timemodified);
262 public function test_delete(): void {
263 global $DB;
265 $this->assign_capability('moodle/category:manage');
266 $this->assign_capability('moodle/course:create');
268 $initialcatid = $DB->get_field_sql('SELECT max(id) from {course_categories}');
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));
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));
280 // Now we have
281 // $category1
282 // $category2
283 // $category4
284 // $course2
285 // $course3
286 // $course1
287 // $course4
288 // $category3
289 // structure.
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());
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);
302 // Make sure we have:
303 // $category1
304 // $course4
305 // $category3
306 // $category4
307 // $course2
308 // $course3
309 // $course1
310 // structure.
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)));
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();
326 // Make sure we have:
327 // $category1
328 // $course4
329 // structure.
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)));
339 public function test_get_children(): void {
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));
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());
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());
359 $children = $category1->get_children(array('limit' => 2));
360 $this->assertEquals(array($category2->id, $category4->id), array_keys($children));
362 $children = $category1->get_children(array('offset' => 1, 'limit' => 2));
363 $this->assertEquals(array($category4->id, $category6->id), array_keys($children));
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));
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));
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());
381 * Test the get_all_children_ids function.
383 public function test_get_all_children_ids(): void {
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));
393 $this->assertCount(0, $category2->get_all_children_ids());
394 $this->assertCount(6, $category1->get_all_children_ids());
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);
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());
408 $this->assertEquals(array($category1121->id), $category112->get_all_children_ids());
412 * Test the countall function
414 public function test_count_all(): void {
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);
436 * Test the is_simple_site function
438 public function test_is_simple_site(): void {
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());
453 * Test a categories ability to resort courses.
455 public function test_resort_courses(): void {
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'
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'
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'
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'
487 $c1 = (int)$course1->id;
488 $c2 = (int)$course2->id;
489 $c3 = (int)$course3->id;
490 $c4 = (int)$course4->id;
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()));
496 $this->assertTrue($coursecat->resort_courses('shortname'));
497 $this->assertSame(array($c1, $c2, $c4, $c3), array_keys($coursecat->get_courses()));
499 $this->assertTrue($coursecat->resort_courses('timecreated'));
500 $this->assertSame(array($c1, $c2, $c3, $c4), array_keys($coursecat->get_courses()));
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);
511 $this->assertTrue($coursecat->resort_courses('fullname'));
512 $this->assertSame($expected, array_keys($coursecat->get_courses()));
515 public function test_get_search_courses(): void {
516 global $DB;
518 $cat1 = core_course_category::create(array('name' => 'Cat1'));
519 $cat2 = core_course_category::create(array('name' => 'Cat2', 'parent' => $cat1->id));
520 $c1 = $this->getDataGenerator()->create_course(array('category' => $cat1->id, 'fullname' => 'Test 3', 'summary' => ' ', 'idnumber' => 'ID3'));
521 $c2 = $this->getDataGenerator()->create_course(array('category' => $cat1->id, 'fullname' => 'Test 1', 'summary' => ' ', 'visible' => 0));
522 $c3 = $this->getDataGenerator()->create_course(array('category' => $cat1->id, 'fullname' => 'Математика', 'summary' => ' Test '));
523 $c4 = $this->getDataGenerator()->create_course(array('category' => $cat1->id, 'fullname' => 'Test 4', 'summary' => ' ', 'idnumber' => 'ID4'));
525 $c5 = $this->getDataGenerator()->create_course(array('category' => $cat2->id, 'fullname' => 'Test 5', 'summary' => ' '));
526 $c6 = $this->getDataGenerator()->create_course(array('category' => $cat2->id, 'fullname' => 'Дискретная Математика', 'summary' => ' '));
527 $c7 = $this->getDataGenerator()->create_course(array('category' => $cat2->id, 'fullname' => 'Test 7', 'summary' => ' ', 'visible' => 0));
528 $c8 = $this->getDataGenerator()->create_course(array('category' => $cat2->id, 'fullname' => 'Test 8', 'summary' => ' '));
530 // Get courses in category 1 (returned visible only because user is not enrolled).
531 $res = $cat1->get_courses(array('sortorder' => 1));
532 $this->assertEquals(array($c4->id, $c3->id, $c1->id), array_keys($res)); // Courses are added in reverse order.
533 $this->assertEquals(3, $cat1->get_courses_count());
535 // Get courses in category 1 recursively (returned visible only because user is not enrolled).
536 $res = $cat1->get_courses(array('recursive' => 1));
537 $this->assertEquals(array($c4->id, $c3->id, $c1->id, $c8->id, $c6->id, $c5->id), array_keys($res));
538 $this->assertEquals(6, $cat1->get_courses_count(array('recursive' => 1)));
540 // Get courses sorted by fullname.
541 $res = $cat1->get_courses(array('sort' => array('fullname' => 1)));
542 $this->assertEquals(array($c1->id, $c4->id, $c3->id), array_keys($res));
543 $this->assertEquals(3, $cat1->get_courses_count(array('sort' => array('fullname' => 1))));
545 // Get courses sorted by fullname recursively.
546 $res = $cat1->get_courses(array('recursive' => 1, 'sort' => array('fullname' => 1)));
547 $this->assertEquals(array($c1->id, $c4->id, $c5->id, $c8->id, $c6->id, $c3->id), array_keys($res));
548 $this->assertEquals(6, $cat1->get_courses_count(array('recursive' => 1, 'sort' => array('fullname' => 1))));
550 // Get courses sorted by fullname recursively, use offset and limit.
551 $res = $cat1->get_courses(array('recursive' => 1, 'offset' => 1, 'limit' => 2, 'sort' => array('fullname' => -1)));
552 $this->assertEquals(array($c6->id, $c8->id), array_keys($res));
553 // Offset and limit do not affect get_courses_count().
554 $this->assertEquals(6, $cat1->get_courses_count(array('recursive' => 1, 'offset' => 1, 'limit' => 2, 'sort' => array('fullname' => 1))));
556 // Calling get_courses_count without prior call to get_courses().
557 $this->assertEquals(3, $cat2->get_courses_count(array('recursive' => 1, 'sort' => array('idnumber' => 1))));
559 // Search courses.
561 // Search by text.
562 $res = core_course_category::search_courses(array('search' => 'Test'));
563 $this->assertEquals(array($c4->id, $c3->id, $c1->id, $c8->id, $c5->id), array_keys($res));
564 $this->assertEquals(5, core_course_category::search_courses_count(array('search' => 'Test')));
566 // Search by text with specified offset and limit.
567 $options = array('sort' => array('fullname' => 1), 'offset' => 1, 'limit' => 2);
568 $res = core_course_category::search_courses(array('search' => 'Test'), $options);
569 $this->assertEquals(array($c4->id, $c5->id), array_keys($res));
570 $this->assertEquals(5, core_course_category::search_courses_count(array('search' => 'Test'), $options));
572 // IMPORTANT: the tests below may fail on some databases
573 // case-insensitive search.
574 $res = core_course_category::search_courses(array('search' => 'test'));
575 $this->assertEquals(array($c4->id, $c3->id, $c1->id, $c8->id, $c5->id), array_keys($res));
576 $this->assertEquals(5, core_course_category::search_courses_count(array('search' => 'test')));
578 // Non-latin language search.
579 $res = core_course_category::search_courses(array('search' => 'Математика'));
580 $this->assertEquals(array($c3->id, $c6->id), array_keys($res));
581 $this->assertEquals(2, core_course_category::search_courses_count(array('search' => 'Математика'), array()));
583 $user = $this->getDataGenerator()->create_user();
584 $this->setUser($user);
586 // Add necessary capabilities.
587 $this->assign_capability('moodle/course:create', CAP_ALLOW, \context_coursecat::instance($cat2->id));
588 // Do another search with restricted capabilities.
589 $reqcaps = array('moodle/course:create');
590 $res = core_course_category::search_courses(array('search' => 'test'), array(), $reqcaps);
591 $this->assertEquals(array($c8->id, $c5->id), array_keys($res));
592 $this->assertEquals(2, core_course_category::search_courses_count(array('search' => 'test'), array(), $reqcaps));
594 // We should get no courses here as user is not enrolled to any courses.
595 $res = core_course_category::search_courses([
596 'search' => '',
597 'limittoenrolled' => 1,
599 $this->assertEquals([], $res);
600 $this->assertEquals(0, core_course_category::search_courses_count([
601 'search' => '',
602 'limittoenrolled' => 1,
603 ]));
605 $manual = enrol_get_plugin('manual');
606 $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
607 $enrol = $DB->get_record('enrol', ['courseid' => $c5->id, 'enrol' => 'manual'], '*', MUST_EXIST);
608 $manual->enrol_user($enrol, $user->id, $teacherrole->id);
610 // Avoid using the cached values from previous method call.
611 \cache::make('core', 'coursecat')->purge();
613 // As the user is now enrolled, we should get this one course.
614 $res = core_course_category::search_courses([
615 'search' => '',
616 'limittoenrolled' => 1,
618 $this->assertEquals([$c5->id], array_keys($res));
619 $this->assertEquals(1, core_course_category::search_courses_count([
620 'search' => '',
621 'limittoenrolled' => 1,
622 ]));
625 public function test_course_contacts(): void {
626 global $DB, $CFG;
628 set_config('coursecontactduplicates', false);
630 $teacherrole = $DB->get_record('role', array('shortname'=>'editingteacher'));
631 $managerrole = $DB->get_record('role', array('shortname'=>'manager'));
632 $studentrole = $DB->get_record('role', array('shortname'=>'student'));
633 $oldcoursecontact = $CFG->coursecontact;
635 $CFG->coursecontact = $managerrole->id. ','. $teacherrole->id;
638 * User is listed in course contacts for the course if he has one of the
639 * "course contact" roles ($CFG->coursecontact) AND is enrolled in the course.
640 * If the user has several roles only the highest is displayed.
643 // Test case:
645 // == Cat1 (user2 has teacher role)
646 // == Cat2
647 // -- course21 (user2 is enrolled as manager) | [Expected] Manager: F2 L2
648 // -- course22 (user2 is enrolled as student) | [Expected] Teacher: F2 L2
649 // == Cat4 (user2 has manager role)
650 // -- course41 (user4 is enrolled as teacher, user5 is enrolled as manager) | [Expected] Manager: F5 L5, Teacher: F4 L4
651 // -- course42 (user2 is enrolled as teacher) | [Expected] Manager: F2 L2
652 // == Cat3 (user3 has manager role)
653 // -- course31 (user3 is enrolled as student) | [Expected] Manager: F3 L3
654 // -- course32 | [Expected]
655 // -- course11 (user1 is enrolled as teacher) | [Expected] Teacher: F1 L1
656 // -- course12 (user1 has teacher role) | [Expected]
657 // also user4 is enrolled as teacher but enrolment is not active
658 $category = $course = $enrol = $user = array();
659 $category[1] = core_course_category::create(array('name' => 'Cat1'))->id;
660 $category[2] = core_course_category::create(array('name' => 'Cat2', 'parent' => $category[1]))->id;
661 $category[3] = core_course_category::create(array('name' => 'Cat3', 'parent' => $category[1]))->id;
662 $category[4] = core_course_category::create(array('name' => 'Cat4', 'parent' => $category[2]))->id;
663 foreach (array(1, 2, 3, 4) as $catid) {
664 foreach (array(1, 2) as $courseid) {
665 $course[$catid][$courseid] = $this->getDataGenerator()->create_course(array('idnumber' => 'id'.$catid.$courseid,
666 'category' => $category[$catid]))->id;
667 $enrol[$catid][$courseid] = $DB->get_record('enrol', array('courseid'=>$course[$catid][$courseid], 'enrol'=>'manual'), '*', MUST_EXIST);
670 foreach (array(1, 2, 3, 4, 5) as $userid) {
671 $user[$userid] = $this->getDataGenerator()->create_user(array('firstname' => 'F'.$userid, 'lastname' => 'L'.$userid))->id;
674 $manual = enrol_get_plugin('manual');
676 // Nobody is enrolled now and course contacts are empty.
677 $allcourses = core_course_category::get(0)->get_courses(
678 array('recursive' => true, 'coursecontacts' => true, 'sort' => array('idnumber' => 1)));
679 foreach ($allcourses as $onecourse) {
680 $this->assertEmpty($onecourse->get_course_contacts());
683 // Cat1 (user2 has teacher role)
684 role_assign($teacherrole->id, $user[2], \context_coursecat::instance($category[1]));
685 // course21 (user2 is enrolled as manager)
686 $manual->enrol_user($enrol[2][1], $user[2], $managerrole->id);
687 // course22 (user2 is enrolled as student)
688 $manual->enrol_user($enrol[2][2], $user[2], $studentrole->id);
689 // Cat4 (user2 has manager role)
690 role_assign($managerrole->id, $user[2], \context_coursecat::instance($category[4]));
691 // course41 (user4 is enrolled as teacher, user5 is enrolled as manager)
692 $manual->enrol_user($enrol[4][1], $user[4], $teacherrole->id);
693 $manual->enrol_user($enrol[4][1], $user[5], $managerrole->id);
694 // course42 (user2 is enrolled as teacher)
695 $manual->enrol_user($enrol[4][2], $user[2], $teacherrole->id);
696 // Cat3 (user3 has manager role)
697 role_assign($managerrole->id, $user[3], \context_coursecat::instance($category[3]));
698 // course31 (user3 is enrolled as student)
699 $manual->enrol_user($enrol[3][1], $user[3], $studentrole->id);
700 // course11 (user1 is enrolled as teacher)
701 $manual->enrol_user($enrol[1][1], $user[1], $teacherrole->id);
702 // -- course12 (user1 has teacher role)
703 // also user4 is enrolled as teacher but enrolment is not active
704 role_assign($teacherrole->id, $user[1], \context_course::instance($course[1][2]));
705 $manual->enrol_user($enrol[1][2], $user[4], $teacherrole->id, 0, 0, ENROL_USER_SUSPENDED);
707 $allcourses = core_course_category::get(0)->get_courses(
708 array('recursive' => true, 'coursecontacts' => true, 'sort' => array('idnumber' => 1)));
709 // Simplify the list of contacts for each course (similar as renderer would do).
710 $contacts = array();
711 foreach (array(1, 2, 3, 4) as $catid) {
712 foreach (array(1, 2) as $courseid) {
713 $tmp = array();
714 foreach ($allcourses[$course[$catid][$courseid]]->get_course_contacts() as $contact) {
715 $tmp[] = $contact['rolename']. ': '. $contact['username'];
717 $contacts[$catid][$courseid] = join(', ', $tmp);
721 // Assert:
722 // -- course21 (user2 is enrolled as manager) | Manager: F2 L2
723 $this->assertSame('Manager: F2 L2', $contacts[2][1]);
724 // -- course22 (user2 is enrolled as student) | Teacher: F2 L2
725 $this->assertSame('Teacher: F2 L2', $contacts[2][2]);
726 // -- course41 (user4 is enrolled as teacher, user5 is enrolled as manager) | Manager: F5 L5, Teacher: F4 L4
727 $this->assertSame('Manager: F5 L5, Teacher: F4 L4', $contacts[4][1]);
728 // -- course42 (user2 is enrolled as teacher) | [Expected] Manager: F2 L2
729 $this->assertSame('Manager: F2 L2', $contacts[4][2]);
730 // -- course31 (user3 is enrolled as student) | Manager: F3 L3
731 $this->assertSame('Manager: F3 L3', $contacts[3][1]);
732 // -- course32 |
733 $this->assertSame('', $contacts[3][2]);
734 // -- course11 (user1 is enrolled as teacher) | Teacher: F1 L1
735 $this->assertSame('Teacher: F1 L1', $contacts[1][1]);
736 // -- course12 (user1 has teacher role) |
737 $this->assertSame('', $contacts[1][2]);
739 // Suspend user 4 and make sure he is no longer in contacts of course 1 in category 4.
740 $manual->enrol_user($enrol[4][1], $user[4], $teacherrole->id, 0, 0, ENROL_USER_SUSPENDED);
741 $allcourses = core_course_category::get(0)->get_courses(array(
742 'recursive' => true,
743 'coursecontacts' => true,
744 'sort' => array('idnumber' => 1))
746 $contacts = $allcourses[$course[4][1]]->get_course_contacts();
747 $this->assertCount(1, $contacts);
748 $contact = reset($contacts);
749 $this->assertEquals('F5 L5', $contact['username']);
751 $CFG->coursecontact = $oldcoursecontact;
754 public function test_course_contacts_with_duplicates(): void {
755 global $DB, $CFG;
757 set_config('coursecontactduplicates', true);
759 $displayall = get_config('core', 'coursecontactduplicates');
760 $this->assertEquals(true, $displayall);
762 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
763 $managerrole = $DB->get_record('role', array('shortname' => 'manager'));
764 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
765 $oldcoursecontact = $CFG->coursecontact;
767 $CFG->coursecontact = $managerrole->id. ','. $teacherrole->id;
770 * User is listed in course contacts for the course if he has one of the
771 * "course contact" roles ($CFG->coursecontact) AND is enrolled in the course.
772 * If the user has several roles all roles are displayed, but each role only once per user.
776 * Test case:
778 * == Cat1 (user2 has teacher role)
779 * == Cat2
780 * -- course21 (user2 is enrolled as manager) | [Expected] Manager: F2 L2
781 * -- course22 (user2 is enrolled as student) | [Expected] Teacher: F2 L2
782 * == Cat4 (user2 has manager role)
783 * -- course41 (user4 is enrolled as teacher, user5 is enrolled as manager)
784 * | [Expected] Manager: F5 L5, Teacher: F4 L4
785 * -- course42 (user2 is enrolled as teacher) | [Expected] Manager: F2 L2
786 * == Cat3 (user3 has manager role)
787 * -- course31 (user3 is enrolled as student) | [Expected] Manager: F3 L3
788 * -- course32 | [Expected]
789 * -- course11 (user1 is enrolled as teacher) | [Expected] Teacher: F1 L1
790 * -- course12 (user1 has teacher role) | [Expected]
791 * also user4 is enrolled as teacher but enrolment is not active
793 $category = $course = $enrol = $user = array();
794 $category[1] = core_course_category::create(array('name' => 'Cat1'))->id;
795 $category[2] = core_course_category::create(array('name' => 'Cat2', 'parent' => $category[1]))->id;
796 $category[3] = core_course_category::create(array('name' => 'Cat3', 'parent' => $category[1]))->id;
797 $category[4] = core_course_category::create(array('name' => 'Cat4', 'parent' => $category[2]))->id;
798 foreach (array(1, 2, 3, 4) as $catid) {
799 foreach (array(1, 2) as $courseid) {
800 $course[$catid][$courseid] = $this->getDataGenerator()->create_course(array(
801 'idnumber' => 'id'.$catid.$courseid,
802 'category' => $category[$catid])
803 )->id;
804 $enrol[$catid][$courseid] = $DB->get_record(
805 'enrol',
806 array('courseid' => $course[$catid][$courseid], 'enrol' => 'manual'),
807 '*',
808 MUST_EXIST
812 foreach (array(1, 2, 3, 4, 5) as $userid) {
813 $user[$userid] = $this->getDataGenerator()->create_user(array(
814 'firstname' => 'F'.$userid,
815 'lastname' => 'L'.$userid)
816 )->id;
819 $manual = enrol_get_plugin('manual');
821 // Nobody is enrolled now and course contacts are empty.
822 $allcourses = core_course_category::get(0)->get_courses(array(
823 'recursive' => true,
824 'coursecontacts' => true,
825 'sort' => array('idnumber' => 1))
827 foreach ($allcourses as $onecourse) {
828 $this->assertEmpty($onecourse->get_course_contacts());
831 // Cat1: user2 has teacher role.
832 role_assign($teacherrole->id, $user[2], \context_coursecat::instance($category[1]));
833 // Course21: user2 is enrolled as manager.
834 $manual->enrol_user($enrol[2][1], $user[2], $managerrole->id);
835 // Course22: user2 is enrolled as student.
836 $manual->enrol_user($enrol[2][2], $user[2], $studentrole->id);
837 // Cat4: user2 has manager role.
838 role_assign($managerrole->id, $user[2], \context_coursecat::instance($category[4]));
839 // Course41: user4 is enrolled as teacher, user5 is enrolled as manager.
840 $manual->enrol_user($enrol[4][1], $user[4], $teacherrole->id);
841 $manual->enrol_user($enrol[4][1], $user[5], $managerrole->id);
842 // Course42: user2 is enrolled as teacher.
843 $manual->enrol_user($enrol[4][2], $user[2], $teacherrole->id);
844 // Cat3: user3 has manager role.
845 role_assign($managerrole->id, $user[3], \context_coursecat::instance($category[3]));
846 // Course31: user3 is enrolled as student.
847 $manual->enrol_user($enrol[3][1], $user[3], $studentrole->id);
848 // Course11: user1 is enrolled as teacher and user4 is enrolled as teacher and has manager role.
849 $manual->enrol_user($enrol[1][1], $user[1], $teacherrole->id);
850 $manual->enrol_user($enrol[1][1], $user[4], $teacherrole->id);
851 role_assign($managerrole->id, $user[4], \context_course::instance($course[1][1]));
852 // Course12: user1 has teacher role, but is not enrolled, as well as user4 is enrolled as teacher, but user4's enrolment is
853 // not active.
854 role_assign($teacherrole->id, $user[1], \context_course::instance($course[1][2]));
855 $manual->enrol_user($enrol[1][2], $user[4], $teacherrole->id, 0, 0, ENROL_USER_SUSPENDED);
857 $allcourses = core_course_category::get(0)->get_courses(
858 array('recursive' => true, 'coursecontacts' => true, 'sort' => array('idnumber' => 1)));
859 // Simplify the list of contacts for each course (similar as renderer would do).
860 $contacts = array();
861 foreach (array(1, 2, 3, 4) as $catid) {
862 foreach (array(1, 2) as $courseid) {
863 $tmp = array();
864 foreach ($allcourses[$course[$catid][$courseid]]->get_course_contacts() as $contact) {
865 $rolenames = array_map(function ($role) {
866 return $role->displayname;
867 }, $contact['roles']);
868 $tmp[] = implode(", ", $rolenames). ': '.
869 $contact['username'];
871 $contacts[$catid][$courseid] = join(', ', $tmp);
875 // Assert:
876 // Course21: user2 is enrolled as manager. [Expected] Manager: F2 L2, Teacher: F2 L2.
877 $this->assertSame('Manager, Teacher: F2 L2', $contacts[2][1]);
878 // Course22: user2 is enrolled as student. [Expected] Teacher: F2 L2.
879 $this->assertSame('Teacher: F2 L2', $contacts[2][2]);
880 // Course41: user4 is enrolled as teacher, user5 is enrolled as manager. [Expected] Manager: F5 L5, Teacher: F4 L4.
881 $this->assertSame('Manager: F5 L5, Teacher: F4 L4', $contacts[4][1]);
882 // Course42: user2 is enrolled as teacher. [Expected] Manager: F2 L2, Teacher: F2 L2.
883 $this->assertSame('Manager, Teacher: F2 L2', $contacts[4][2]);
884 // Course31: user3 is enrolled as student. [Expected] Manager: F3 L3.
885 $this->assertSame('Manager: F3 L3', $contacts[3][1]);
886 // Course32: nobody is enrolled. [Expected] (nothing).
887 $this->assertSame('', $contacts[3][2]);
888 // Course11: user1 is enrolled as teacher and user4 is enrolled as teacher and has manager role. [Expected] Manager: F4 L4,
889 // Teacher: F1 L1, Teacher: F4 L4.
890 $this->assertSame('Manager, Teacher: F4 L4, Teacher: F1 L1', $contacts[1][1]);
891 // Course12: user1 has teacher role, but is not enrolled, as well as user4 is enrolled as teacher, but user4's enrolment is
892 // not active. [Expected] (nothing).
893 $this->assertSame('', $contacts[1][2]);
895 // Suspend user 4 and make sure he is no longer in contacts of course 1 in category 4.
896 $manual->enrol_user($enrol[4][1], $user[4], $teacherrole->id, 0, 0, ENROL_USER_SUSPENDED);
897 $allcourses = core_course_category::get(0)->get_courses(array(
898 'recursive' => true,
899 'coursecontacts' => true,
900 'sort' => array('idnumber' => 1)
902 $contacts = $allcourses[$course[4][1]]->get_course_contacts();
903 $this->assertCount(1, $contacts);
904 $contact = reset($contacts);
905 $this->assertEquals('F5 L5', $contact['username']);
907 $CFG->coursecontact = $oldcoursecontact;
910 public function test_overview_files(): void {
911 global $CFG;
912 $this->setAdminUser();
913 $cat1 = core_course_category::create(array('name' => 'Cat1'));
915 // Create course c1 with one image file.
916 $dratid1 = $this->fill_draft_area(array('filename.jpg' => 'Test file contents1'));
917 $c1 = $this->getDataGenerator()->create_course(array('category' => $cat1->id,
918 'fullname' => 'Test 1', 'overviewfiles_filemanager' => $dratid1));
919 // Create course c2 with two image files (only one file will be added because of settings).
920 $dratid2 = $this->fill_draft_area(array('filename21.jpg' => 'Test file contents21', 'filename22.jpg' => 'Test file contents22'));
921 $c2 = $this->getDataGenerator()->create_course(array('category' => $cat1->id,
922 'fullname' => 'Test 2', 'overviewfiles_filemanager' => $dratid2));
923 // Create course c3 without files.
924 $c3 = $this->getDataGenerator()->create_course(array('category' => $cat1->id, 'fullname' => 'Test 3'));
926 // Change the settings to allow multiple files of any types.
927 $CFG->courseoverviewfileslimit = 3;
928 $CFG->courseoverviewfilesext = '*';
929 // Create course c5 with two image files.
930 $dratid4 = $this->fill_draft_area(array('filename41.jpg' => 'Test file contents41', 'filename42.jpg' => 'Test file contents42'));
931 $c4 = $this->getDataGenerator()->create_course(array('category' => $cat1->id,
932 'fullname' => 'Test 4', 'overviewfiles_filemanager' => $dratid4));
933 // Create course c6 with non-image file.
934 $dratid5 = $this->fill_draft_area(array('filename51.zip' => 'Test file contents51'));
935 $c5 = $this->getDataGenerator()->create_course(array('category' => $cat1->id,
936 'fullname' => 'Test 5', 'overviewfiles_filemanager' => $dratid5));
938 // Reset default settings.
939 $CFG->courseoverviewfileslimit = 1;
940 $CFG->courseoverviewfilesext = 'web_image';
942 $courses = $cat1->get_courses();
943 $this->assertTrue($courses[$c1->id]->has_course_overviewfiles());
944 $this->assertTrue($courses[$c2->id]->has_course_overviewfiles());
945 $this->assertFalse($courses[$c3->id]->has_course_overviewfiles());
946 $this->assertTrue($courses[$c4->id]->has_course_overviewfiles());
947 $this->assertTrue($courses[$c5->id]->has_course_overviewfiles()); // Does not validate the filetypes.
949 $this->assertEquals(1, count($courses[$c1->id]->get_course_overviewfiles()));
950 $this->assertEquals(1, count($courses[$c2->id]->get_course_overviewfiles()));
951 $this->assertEquals(0, count($courses[$c3->id]->get_course_overviewfiles()));
952 $this->assertEquals(1, count($courses[$c4->id]->get_course_overviewfiles()));
953 $this->assertEquals(0, count($courses[$c5->id]->get_course_overviewfiles())); // Validate the filetypes.
955 // Overview files are not allowed, all functions return empty values.
956 $CFG->courseoverviewfileslimit = 0;
958 $this->assertFalse($courses[$c1->id]->has_course_overviewfiles());
959 $this->assertFalse($courses[$c2->id]->has_course_overviewfiles());
960 $this->assertFalse($courses[$c3->id]->has_course_overviewfiles());
961 $this->assertFalse($courses[$c4->id]->has_course_overviewfiles());
962 $this->assertFalse($courses[$c5->id]->has_course_overviewfiles());
964 $this->assertEquals(0, count($courses[$c1->id]->get_course_overviewfiles()));
965 $this->assertEquals(0, count($courses[$c2->id]->get_course_overviewfiles()));
966 $this->assertEquals(0, count($courses[$c3->id]->get_course_overviewfiles()));
967 $this->assertEquals(0, count($courses[$c4->id]->get_course_overviewfiles()));
968 $this->assertEquals(0, count($courses[$c5->id]->get_course_overviewfiles()));
970 // Multiple overview files are allowed but still limited to images.
971 $CFG->courseoverviewfileslimit = 3;
973 $this->assertTrue($courses[$c1->id]->has_course_overviewfiles());
974 $this->assertTrue($courses[$c2->id]->has_course_overviewfiles());
975 $this->assertFalse($courses[$c3->id]->has_course_overviewfiles());
976 $this->assertTrue($courses[$c4->id]->has_course_overviewfiles());
977 $this->assertTrue($courses[$c5->id]->has_course_overviewfiles()); // Still does not validate the filetypes.
979 $this->assertEquals(1, count($courses[$c1->id]->get_course_overviewfiles()));
980 $this->assertEquals(1, count($courses[$c2->id]->get_course_overviewfiles())); // Only 1 file was actually added.
981 $this->assertEquals(0, count($courses[$c3->id]->get_course_overviewfiles()));
982 $this->assertEquals(2, count($courses[$c4->id]->get_course_overviewfiles()));
983 $this->assertEquals(0, count($courses[$c5->id]->get_course_overviewfiles()));
985 // Multiple overview files of any type are allowed.
986 $CFG->courseoverviewfilesext = '*';
988 $this->assertTrue($courses[$c1->id]->has_course_overviewfiles());
989 $this->assertTrue($courses[$c2->id]->has_course_overviewfiles());
990 $this->assertFalse($courses[$c3->id]->has_course_overviewfiles());
991 $this->assertTrue($courses[$c4->id]->has_course_overviewfiles());
992 $this->assertTrue($courses[$c5->id]->has_course_overviewfiles());
994 $this->assertEquals(1, count($courses[$c1->id]->get_course_overviewfiles()));
995 $this->assertEquals(1, count($courses[$c2->id]->get_course_overviewfiles()));
996 $this->assertEquals(0, count($courses[$c3->id]->get_course_overviewfiles()));
997 $this->assertEquals(2, count($courses[$c4->id]->get_course_overviewfiles()));
998 $this->assertEquals(1, count($courses[$c5->id]->get_course_overviewfiles()));
1001 public function test_get_nested_name(): void {
1002 $cat1name = 'Cat1';
1003 $cat2name = 'Cat2';
1004 $cat3name = 'Cat3';
1005 $cat4name = 'Cat4';
1006 $category1 = core_course_category::create(array('name' => $cat1name));
1007 $category2 = core_course_category::create(array('name' => $cat2name, 'parent' => $category1->id));
1008 $category3 = core_course_category::create(array('name' => $cat3name, 'parent' => $category2->id));
1009 $category4 = core_course_category::create(array('name' => $cat4name, 'parent' => $category2->id));
1011 $this->assertEquals($cat1name, $category1->get_nested_name(false));
1012 $this->assertEquals("{$cat1name} / {$cat2name}", $category2->get_nested_name(false));
1013 $this->assertEquals("{$cat1name} / {$cat2name} / {$cat3name}", $category3->get_nested_name(false));
1014 $this->assertEquals("{$cat1name} / {$cat2name} / {$cat4name}", $category4->get_nested_name(false));
1017 public function test_coursecat_is_uservisible(): void {
1018 global $USER;
1020 // Create category 1 as visible.
1021 $category1 = core_course_category::create(array('name' => 'Cat1', 'visible' => 1));
1022 // Create category 2 as hidden.
1023 $category2 = core_course_category::create(array('name' => 'Cat2', 'visible' => 0));
1025 $this->assertTrue($category1->is_uservisible());
1026 $this->assertFalse($category2->is_uservisible());
1028 $this->assign_capability('moodle/category:viewhiddencategories');
1030 $this->assertTrue($category1->is_uservisible());
1031 $this->assertTrue($category2->is_uservisible());
1033 // First, store current user's id, then login as another user.
1034 $userid = $USER->id;
1035 $this->setUser($this->getDataGenerator()->create_user());
1037 // User $user should still have the moodle/category:viewhiddencategories capability.
1038 $this->assertTrue($category1->is_uservisible($userid));
1039 $this->assertTrue($category2->is_uservisible($userid));
1041 $this->assign_capability('moodle/category:viewhiddencategories', CAP_INHERIT);
1043 $this->assertTrue($category1->is_uservisible());
1044 $this->assertFalse($category2->is_uservisible());
1047 public function test_current_user_coursecat_get(): void {
1048 $this->assign_capability('moodle/category:viewhiddencategories');
1050 // Create category 1 as visible.
1051 $category1 = core_course_category::create(array('name' => 'Cat1', 'visible' => 1));
1052 // Create category 2 as hidden.
1053 $category2 = core_course_category::create(array('name' => 'Cat2', 'visible' => 0));
1055 $this->assertEquals($category1->id, core_course_category::get($category1->id)->id);
1056 $this->assertEquals($category2->id, core_course_category::get($category2->id)->id);
1058 // Login as another user to test core_course_category::get.
1059 $this->setUser($this->getDataGenerator()->create_user());
1060 $this->assertEquals($category1->id, core_course_category::get($category1->id)->id);
1062 // Expecting to get an exception as this new user does not have the moodle/category:viewhiddencategories capability.
1063 $this->expectException('moodle_exception');
1064 $this->expectExceptionMessage(get_string('cannotviewcategory', 'error'));
1065 core_course_category::get($category2->id);
1068 public function test_another_user_coursecat_get(): void {
1069 global $USER;
1071 $this->assign_capability('moodle/category:viewhiddencategories');
1073 // Create category 1 as visible.
1074 $category1 = core_course_category::create(array('name' => 'Cat1', 'visible' => 1));
1075 // Create category 2 as hidden.
1076 $category2 = core_course_category::create(array('name' => 'Cat2', 'visible' => 0));
1078 // First, store current user's object, then login as another user.
1079 $user1 = $USER;
1080 $user2 = $this->getDataGenerator()->create_user();
1081 $this->setUser($user2);
1083 $this->assertEquals($category1->id, core_course_category::get($category1->id, MUST_EXIST, false, $user1)->id);
1084 $this->assertEquals($category2->id, core_course_category::get($category2->id, MUST_EXIST, false, $user1)->id);
1086 $this->setUser($user1);
1088 $this->assertEquals($category1->id, core_course_category::get($category1->id, MUST_EXIST, false, $user2)->id);
1089 $this->expectException('moodle_exception');
1090 $this->expectExceptionMessage(get_string('cannotviewcategory', 'error'));
1091 core_course_category::get($category2->id, MUST_EXIST, false, $user2);
1095 * Creates a draft area for current user and fills it with fake files
1097 * @param array $files array of files that need to be added to filearea, filename => filecontents
1098 * @return int draftid for the filearea
1100 protected function fill_draft_area(array $files) {
1101 global $USER;
1102 $usercontext = \context_user::instance($USER->id);
1103 $draftid = file_get_unused_draft_itemid();
1104 foreach ($files as $filename => $filecontents) {
1105 // Add actual file there.
1106 $filerecord = array('component' => 'user', 'filearea' => 'draft',
1107 'contextid' => $usercontext->id, 'itemid' => $draftid,
1108 'filename' => $filename, 'filepath' => '/');
1109 $fs = get_file_storage();
1110 $fs->create_file_from_string($filerecord, $filecontents);
1112 return $draftid;
1116 * This test ensures that is the list of courses in a category can be retrieved while a course is being deleted.
1118 public function test_get_courses_during_delete(): void {
1119 global $DB;
1120 $category = self::getDataGenerator()->create_category();
1121 $course = self::getDataGenerator()->create_course(['category' => $category->id]);
1122 $othercourse = self::getDataGenerator()->create_course(['category' => $category->id]);
1123 $coursecategory = core_course_category::get($category->id);
1124 // Get a list of courses before deletion to populate the cache.
1125 $originalcourses = $coursecategory->get_courses();
1126 $this->assertCount(2, $originalcourses);
1127 $this->assertArrayHasKey($course->id, $originalcourses);
1128 $this->assertArrayHasKey($othercourse->id, $originalcourses);
1129 // Simulate the course deletion process being part way though.
1130 $DB->delete_records('course', ['id' => $course->id]);
1131 // Get the list of courses while a deletion is in progress.
1132 $courses = $coursecategory->get_courses();
1133 $this->assertCount(1, $courses);
1134 $this->assertArrayHasKey($othercourse->id, $courses);
1138 * Test get_nearest_editable_subcategory() method.
1140 * @covers \core_course_category::get_nearest_editable_subcategory
1142 public function test_get_nearest_editable_subcategory(): void {
1143 global $DB;
1145 $coursecreatorrole = $DB->get_record('role', ['shortname' => 'coursecreator']);
1146 $managerrole = $DB->get_record('role', ['shortname' => 'manager']);
1148 // Create categories.
1149 $category1 = core_course_category::create(['name' => 'Cat1']);
1150 $category2 = core_course_category::create(['name' => 'Cat2']);
1151 $category3 = core_course_category::create(['name' => 'Cat3']);
1152 // Get the category contexts.
1153 $category1context = $category1->get_context();
1154 $category2context = $category2->get_context();
1155 $category3context = $category3->get_context();
1156 // Create user.
1157 $user1 = $this->getDataGenerator()->create_user();
1158 $user2 = $this->getDataGenerator()->create_user();
1159 $user3 = $this->getDataGenerator()->create_user();
1160 // Assign the user1 to 'Course creator' role for Cat1.
1161 role_assign($coursecreatorrole->id, $user1->id, $category1context->id);
1162 // Assign the user2 to 'Manager' role for Cat3.
1163 role_assign($managerrole->id, $user2->id, $category3context->id);
1165 // Start scenario 1.
1166 // user3 has no permission to create course or manage category.
1167 $this->setUser($user3);
1168 $coursecat = core_course_category::user_top();
1169 $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create']));
1170 $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create']));
1171 $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['manage']));
1172 $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage']));
1173 $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage']));
1174 // End scenario 1.
1176 // Start scenario 2.
1177 // user1 has permission to create course but has no permission to manage category.
1178 $this->setUser($user1);
1179 $coursecat = core_course_category::user_top();
1180 $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create']));
1181 $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create']));
1182 $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['manage']));
1183 $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage']));
1184 $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage']));
1185 // The get_nearest_editable_subcategory should return Cat1.
1186 $this->assertEquals($category1->id, core_course_category::get_nearest_editable_subcategory($coursecat, ['create'])->id);
1187 $this->assertEquals($category1->id,
1188 core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create'])->id);
1189 // Assign the user1 to 'Course creator' role for Cat2.
1190 role_assign($coursecreatorrole->id, $user1->id, $category2context->id);
1191 // The get_nearest_editable_subcategory should still return Cat1 (First creatable subcategory) for create course capability.
1192 $this->assertEquals($category1->id, core_course_category::get_nearest_editable_subcategory($coursecat, ['create'])->id);
1193 $this->assertEquals($category1->id,
1194 core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create'])->id);
1195 // End scenario 2.
1197 // Start scenario 3.
1198 // user2 has no permission to create course but has permission to manage category.
1199 $this->setUser($user2);
1200 // Remove the moodle/course:create capability for the manager role.
1201 unassign_capability('moodle/course:create', $managerrole->id);
1202 $coursecat = core_course_category::user_top();
1203 $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create']));
1204 $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create']));
1205 $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['manage']));
1206 $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage']));
1207 $this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage']));
1208 // The get_nearest_editable_subcategory should return Cat3.
1209 $this->assertEquals($category3->id, core_course_category::get_nearest_editable_subcategory($coursecat, ['manage'])->id);
1210 $this->assertEquals($category3->id,
1211 core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage'])->id);
1212 // End scenario 3.
1214 // Start scenario 4.
1215 // user2 has both permission to create course and manage category.
1216 // Add the moodle/course:create capability back again for the manager role.
1217 assign_capability('moodle/course:create', CAP_ALLOW, $managerrole->id, $category3context->id);
1218 $this->setUser($user2);
1219 $coursecat = core_course_category::user_top();
1220 $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create']));
1221 $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create']));
1222 $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['manage']));
1223 $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage']));
1224 $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage']));
1225 // The get_nearest_editable_subcategory should return Cat3.
1226 $this->assertEquals($category3->id,
1227 core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage'])->id);
1228 $this->assertEquals($category3->id, core_course_category::get_nearest_editable_subcategory($coursecat,
1229 ['moodle/course:create', 'moodle/category:manage'])->id);
1230 // End scenario 4.
1232 // Start scenario 5.
1233 // Exception will be thrown if $permissionstocheck is empty.
1234 $this->setUser($user1);
1235 $coursecat = core_course_category::user_top();
1236 $this->expectException('coding_exception');
1237 $this->expectExceptionMessage('Invalid permissionstocheck parameter');
1238 $this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, []));
1239 // End scenario 5.
1243 * Test get_nearest_editable_subcategory() method with hidden categories.
1245 * @param int $visible Whether the category is visible or not.
1246 * @param bool $child Whether the category is child of main category or not.
1247 * @param string $role The role the user must have.
1248 * @param array $permissions An array of permissions we must check.
1249 * @param bool $result Whether the result should be the category or null.
1251 * @dataProvider get_nearest_editable_subcategory_provider
1252 * @covers \core_course_category::get_nearest_editable_subcategory
1254 public function test_get_nearest_editable_subcategory_with_hidden_categories(
1255 int $visible = 0,
1256 bool $child = false,
1257 string $role = 'manager',
1258 array $permissions = [],
1259 bool $result = false
1260 ): void {
1261 global $DB;
1263 $userrole = $DB->get_record('role', ['shortname' => $role]);
1264 $maincat = core_course_category::create(['name' => 'Main cat']);
1266 $catparams = new \stdClass();
1267 $catparams->name = 'Test category';
1268 $catparams->visible = $visible;
1269 if ($child) {
1270 $catparams->parent = $maincat->id;
1272 $category = core_course_category::create($catparams);
1273 $catcontext = $category->get_context();
1274 $user = $this->getDataGenerator()->create_user();
1275 role_assign($userrole->id, $user->id, $catcontext->id);
1276 $this->setUser($user);
1278 $nearestcat = core_course_category::get_nearest_editable_subcategory(core_course_category::user_top(), $permissions);
1280 if ($result) {
1281 $this->assertEquals($category->id, $nearestcat->id);
1282 } else {
1283 $this->assertEmpty($nearestcat);
1288 * Data provider for test_get_nearest_editable_subcategory_with_hidden_categories().
1290 * @return array
1292 public function get_nearest_editable_subcategory_provider(): array {
1293 return [
1294 'Hidden main category for manager. Checking create and manage' => [
1296 false,
1297 'manager',
1298 ['create', 'manage'],
1299 true,
1301 'Hidden main category for course creator. Checking create and manage' => [
1303 false,
1304 'coursecreator',
1305 ['create', 'manage'],
1306 false,
1308 'Hidden main category for student. Checking create and manage' => [
1310 false,
1311 'student',
1312 ['create', 'manage'],
1313 false,
1315 'Hidden main category for manager. Checking create' => [
1317 false,
1318 'manager',
1319 ['create'],
1320 true,
1322 'Hidden main category for course creator. Checking create' => [
1324 false,
1325 'coursecreator',
1326 ['create'],
1327 true,
1329 'Hidden main category for student. Checking create' => [
1331 false,
1332 'student',
1333 ['create'],
1334 false,
1336 'Hidden subcategory for manager. Checking create and manage' => [
1338 true,
1339 'manager',
1340 ['create', 'manage'],
1341 true,
1343 'Hidden subcategory for course creator. Checking create and manage' => [
1345 true,
1346 'coursecreator',
1347 ['create', 'manage'],
1348 false,
1350 'Hidden subcategory for student. Checking create and manage' => [
1352 true,
1353 'student',
1354 ['create', 'manage'],
1355 false,
1357 'Hidden subcategory for manager. Checking create' => [
1359 true,
1360 'manager',
1361 ['create'],
1362 true,
1364 'Hidden subcategory for course creator. Checking create' => [
1366 true,
1367 'coursecreator',
1368 ['create'],
1369 true,
1371 'Hidden subcategory for student. Checking create' => [
1373 true,
1374 'student',
1375 ['create'],
1376 false,
1382 * This test ensures that the filter context list is populated by the correct filter contexts from make_category_list.
1384 * @coversNothing
1386 public function test_make_category_list_context(): void {
1387 global $DB;
1388 // Ensure that the category list is empty.
1389 $DB->delete_records('course_categories');
1390 set_config('perfdebug', 15);
1392 // Create a few categories to populate the context cache.
1393 $this->getDataGenerator()->create_category(['name' => 'cat1']);
1394 $this->getDataGenerator()->create_category(['name' => 'cat2']);
1395 $this->getDataGenerator()->create_category(['name' => 'cat3']);
1396 $filtermanager = \filter_manager::instance();
1398 // Configure a filter to apply to all content and headings.
1399 filter_set_global_state('multilang', TEXTFILTER_ON);
1400 filter_set_applies_to_strings('multilang', true);
1402 // First test with the performance setting off.
1403 set_config('filternavigationwithsystemcontext', 0);
1405 $perf = $filtermanager->get_performance_summary();
1406 $this->assertEquals(0, $perf[0]['contextswithfilters']);
1408 // Now fill the cache with the category strings.
1409 \core_course_category::make_categories_list();
1410 // 3 Categories + system context.
1411 $perf = $filtermanager->get_performance_summary();
1412 $this->assertEquals(3, $perf[0]['contextswithfilters']);
1413 $filtermanager->reset_caches();
1414 // We need to refresh the instance, resetting caches unloads the singleton.
1415 $filtermanager = \filter_manager::instance();
1416 \cache_helper::purge_by_definition('core', 'coursecat');
1418 // Now flip the bit on the filter context.
1419 set_config('filternavigationwithsystemcontext', 1);
1421 // Repeat the check. Only context should be system context.
1422 \core_course_category::make_categories_list();
1423 $perf = $filtermanager->get_performance_summary();
1424 $this->assertEquals(1, $perf[0]['contextswithfilters']);