MDL-80949 mod_data: Remove unused Allow autolink param for text field
[moodle.git] / course / tests / externallib_test.php
blobce77094a314b3ef85e3212d06a527db62fb2ede1
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 /**
18 * External course functions unit tests
20 * @package core_course
21 * @category external
22 * @copyright 2012 Jerome Mouneyrac
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 use \core_external\external_api;
28 defined('MOODLE_INTERNAL') || die();
30 global $CFG;
32 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
34 /**
35 * External course functions unit tests
37 * @package core_course
38 * @category external
39 * @copyright 2012 Jerome Mouneyrac
40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42 class externallib_test extends externallib_advanced_testcase {
43 //core_course_externallib_testcase
45 /**
46 * Tests set up
48 protected function setUp(): void {
49 global $CFG;
50 require_once($CFG->dirroot . '/course/externallib.php');
53 /**
54 * Test create_categories
56 public function test_create_categories() {
58 global $DB;
60 $this->resetAfterTest(true);
62 // Set the required capabilities by the external function
63 $contextid = context_system::instance()->id;
64 $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
66 // Create base categories.
67 $category1 = new stdClass();
68 $category1->name = 'Root Test Category 1';
69 $category2 = new stdClass();
70 $category2->name = 'Root Test Category 2';
71 $category2->idnumber = 'rootcattest2';
72 $category2->desc = 'Description for root test category 1';
73 $category2->theme = 'classic';
74 $categories = array(
75 array('name' => $category1->name, 'parent' => 0),
76 array('name' => $category2->name, 'parent' => 0, 'idnumber' => $category2->idnumber,
77 'description' => $category2->desc, 'theme' => $category2->theme)
80 $createdcats = core_course_external::create_categories($categories);
82 // We need to execute the return values cleaning process to simulate the web service server.
83 $createdcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdcats);
85 // Initially confirm that base data was inserted correctly.
86 $this->assertEquals($category1->name, $createdcats[0]['name']);
87 $this->assertEquals($category2->name, $createdcats[1]['name']);
89 // Save the ids.
90 $category1->id = $createdcats[0]['id'];
91 $category2->id = $createdcats[1]['id'];
93 // Create on sub category.
94 $category3 = new stdClass();
95 $category3->name = 'Sub Root Test Category 3';
96 $subcategories = array(
97 array('name' => $category3->name, 'parent' => $category1->id)
100 $createdsubcats = core_course_external::create_categories($subcategories);
102 // We need to execute the return values cleaning process to simulate the web service server.
103 $createdsubcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdsubcats);
105 // Confirm that sub categories were inserted correctly.
106 $this->assertEquals($category3->name, $createdsubcats[0]['name']);
108 // Save the ids.
109 $category3->id = $createdsubcats[0]['id'];
111 // Calling the ws function should provide a new sortorder to give category1,
112 // category2, category3. New course categories are ordered by id not name.
113 $category1 = $DB->get_record('course_categories', array('id' => $category1->id));
114 $category2 = $DB->get_record('course_categories', array('id' => $category2->id));
115 $category3 = $DB->get_record('course_categories', array('id' => $category3->id));
117 // sortorder sequence (and sortorder) must be:
118 // category 1
119 // category 3
120 // category 2
121 $this->assertGreaterThan($category1->sortorder, $category3->sortorder);
122 $this->assertGreaterThan($category3->sortorder, $category2->sortorder);
124 // Call without required capability
125 $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
126 $this->expectException('required_capability_exception');
127 $createdsubcats = core_course_external::create_categories($subcategories);
132 * Test delete categories
134 public function test_delete_categories() {
135 global $DB;
137 $this->resetAfterTest(true);
139 // Set the required capabilities by the external function
140 $contextid = context_system::instance()->id;
141 $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
143 $category1 = self::getDataGenerator()->create_category();
144 $category2 = self::getDataGenerator()->create_category(
145 array('parent' => $category1->id));
146 $category3 = self::getDataGenerator()->create_category();
147 $category4 = self::getDataGenerator()->create_category(
148 array('parent' => $category3->id));
149 $category5 = self::getDataGenerator()->create_category(
150 array('parent' => $category4->id));
152 //delete category 1 and 2 + delete category 4, category 5 moved under category 3
153 core_course_external::delete_categories(array(
154 array('id' => $category1->id, 'recursive' => 1),
155 array('id' => $category4->id)
158 //check $category 1 and 2 are deleted
159 $notdeletedcount = $DB->count_records_select('course_categories',
160 'id IN ( ' . $category1->id . ',' . $category2->id . ',' . $category4->id . ')');
161 $this->assertEquals(0, $notdeletedcount);
163 //check that $category5 as $category3 for parent
164 $dbcategory5 = $DB->get_record('course_categories', array('id' => $category5->id));
165 $this->assertEquals($dbcategory5->path, $category3->path . '/' . $category5->id);
167 // Call without required capability
168 $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
169 $this->expectException('required_capability_exception');
170 $createdsubcats = core_course_external::delete_categories(
171 array(array('id' => $category3->id)));
175 * Test get categories
177 public function test_get_categories() {
178 global $DB;
180 $this->resetAfterTest(true);
182 $generatedcats = array();
183 $category1data['idnumber'] = 'idnumbercat1';
184 $category1data['name'] = 'Category 1 for PHPunit test';
185 $category1data['description'] = 'Category 1 description';
186 $category1data['descriptionformat'] = FORMAT_MOODLE;
187 $category1 = self::getDataGenerator()->create_category($category1data);
188 $generatedcats[$category1->id] = $category1;
189 $category2 = self::getDataGenerator()->create_category(
190 array('parent' => $category1->id));
191 $generatedcats[$category2->id] = $category2;
192 $category6 = self::getDataGenerator()->create_category(
193 array('parent' => $category1->id, 'visible' => 0));
194 $generatedcats[$category6->id] = $category6;
195 $category3 = self::getDataGenerator()->create_category();
196 $generatedcats[$category3->id] = $category3;
197 $category4 = self::getDataGenerator()->create_category(
198 array('parent' => $category3->id));
199 $generatedcats[$category4->id] = $category4;
200 $category5 = self::getDataGenerator()->create_category(
201 array('parent' => $category4->id));
202 $generatedcats[$category5->id] = $category5;
204 // Set the required capabilities by the external function.
205 $context = context_system::instance();
206 $roleid = $this->assignUserCapability('moodle/category:manage', $context->id);
207 $this->assignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
209 // Retrieve category1 + sub-categories except not visible ones
210 $categories = core_course_external::get_categories(array(
211 array('key' => 'id', 'value' => $category1->id),
212 array('key' => 'visible', 'value' => 1)), 1);
214 // We need to execute the return values cleaning process to simulate the web service server.
215 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
217 // Check we retrieve the good total number of categories.
218 $this->assertEquals(2, count($categories));
220 // Check the return values
221 foreach ($categories as $category) {
222 $generatedcat = $generatedcats[$category['id']];
223 $this->assertEquals($category['idnumber'], $generatedcat->idnumber);
224 $this->assertEquals($category['name'], $generatedcat->name);
225 // Description was converted to the HTML format.
226 $this->assertEquals($category['description'], format_text($generatedcat->description, FORMAT_MOODLE, array('para' => false)));
227 $this->assertEquals($category['descriptionformat'], FORMAT_HTML);
230 // Check categories by ids.
231 $ids = implode(',', array_keys($generatedcats));
232 $categories = core_course_external::get_categories(array(
233 array('key' => 'ids', 'value' => $ids)), 0);
235 // We need to execute the return values cleaning process to simulate the web service server.
236 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
238 // Check we retrieve the good total number of categories.
239 $this->assertEquals(6, count($categories));
240 // Check ids.
241 $returnedids = [];
242 foreach ($categories as $category) {
243 $returnedids[] = $category['id'];
245 // Sort the arrays upon comparision.
246 $this->assertEqualsCanonicalizing(array_keys($generatedcats), $returnedids);
248 // Check different params.
249 $categories = core_course_external::get_categories(array(
250 array('key' => 'id', 'value' => $category1->id),
251 array('key' => 'ids', 'value' => $category1->id),
252 array('key' => 'idnumber', 'value' => $category1->idnumber),
253 array('key' => 'visible', 'value' => 1)), 0);
255 // We need to execute the return values cleaning process to simulate the web service server.
256 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
258 $this->assertEquals(1, count($categories));
260 // Same query, but forcing a parameters clean.
261 $categories = core_course_external::get_categories(array(
262 array('key' => 'id', 'value' => "$category1->id"),
263 array('key' => 'idnumber', 'value' => $category1->idnumber),
264 array('key' => 'name', 'value' => $category1->name . "<br/>"),
265 array('key' => 'visible', 'value' => '1')), 0);
266 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
268 $this->assertEquals(1, count($categories));
270 // Retrieve categories from parent.
271 $categories = core_course_external::get_categories(array(
272 array('key' => 'parent', 'value' => $category3->id)), 1);
273 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
275 $this->assertEquals(2, count($categories));
277 // Retrieve all categories.
278 $categories = core_course_external::get_categories();
280 // We need to execute the return values cleaning process to simulate the web service server.
281 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
283 $this->assertEquals($DB->count_records('course_categories'), count($categories));
285 $this->unassignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
287 // Ensure maxdepthcategory is 2 and retrieve all categories without category:viewhiddencategories capability.
288 // It should retrieve all visible categories as well.
289 set_config('maxcategorydepth', 2);
290 $categories = core_course_external::get_categories();
292 // We need to execute the return values cleaning process to simulate the web service server.
293 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
295 $this->assertEquals($DB->count_records('course_categories', array('visible' => 1)), count($categories));
297 // Call without required capability (it will fail cause of the search on idnumber).
298 $this->expectException('moodle_exception');
299 $categories = core_course_external::get_categories(array(
300 array('key' => 'id', 'value' => $category1->id),
301 array('key' => 'idnumber', 'value' => $category1->idnumber),
302 array('key' => 'visible', 'value' => 1)), 0);
306 * Test update_categories
308 public function test_update_categories() {
309 global $DB;
311 $this->resetAfterTest(true);
313 // Set the required capabilities by the external function
314 $contextid = context_system::instance()->id;
315 $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
317 // Create base categories.
318 $category1data['idnumber'] = 'idnumbercat1';
319 $category1data['name'] = 'Category 1 for PHPunit test';
320 $category1data['description'] = 'Category 1 description';
321 $category1data['descriptionformat'] = FORMAT_MOODLE;
322 $category1 = self::getDataGenerator()->create_category($category1data);
323 $category2 = self::getDataGenerator()->create_category(
324 array('parent' => $category1->id));
325 $category3 = self::getDataGenerator()->create_category();
326 $category4 = self::getDataGenerator()->create_category(
327 array('parent' => $category3->id));
328 $category5 = self::getDataGenerator()->create_category(
329 array('parent' => $category4->id));
331 // We update all category1 attribut.
332 // Then we move cat4 and cat5 parent: cat3 => cat1
333 $categories = array(
334 array('id' => $category1->id,
335 'name' => $category1->name . '_updated',
336 'idnumber' => $category1->idnumber . '_updated',
337 'description' => $category1->description . '_updated',
338 'descriptionformat' => FORMAT_HTML,
339 'theme' => $category1->theme),
340 array('id' => $category4->id, 'parent' => $category1->id));
342 core_course_external::update_categories($categories);
344 // Check the values were updated.
345 $dbcategories = $DB->get_records_select('course_categories',
346 'id IN (' . $category1->id . ',' . $category2->id . ',' . $category2->id
347 . ',' . $category3->id . ',' . $category4->id . ',' . $category5->id .')');
348 $this->assertEquals($category1->name . '_updated',
349 $dbcategories[$category1->id]->name);
350 $this->assertEquals($category1->idnumber . '_updated',
351 $dbcategories[$category1->id]->idnumber);
352 $this->assertEquals($category1->description . '_updated',
353 $dbcategories[$category1->id]->description);
354 $this->assertEquals(FORMAT_HTML, $dbcategories[$category1->id]->descriptionformat);
356 // Check that category4 and category5 have been properly moved.
357 $this->assertEquals('/' . $category1->id . '/' . $category4->id,
358 $dbcategories[$category4->id]->path);
359 $this->assertEquals('/' . $category1->id . '/' . $category4->id . '/' . $category5->id,
360 $dbcategories[$category5->id]->path);
362 // Call without required capability.
363 $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
364 $this->expectException('required_capability_exception');
365 core_course_external::update_categories($categories);
369 * Test update_categories method for moving categories
371 public function test_update_categories_moving() {
372 $this->resetAfterTest();
374 // Create data.
375 $categorya = self::getDataGenerator()->create_category([
376 'name' => 'CAT_A',
378 $categoryasub = self::getDataGenerator()->create_category([
379 'name' => 'SUBCAT_A',
380 'parent' => $categorya->id
382 $categoryb = self::getDataGenerator()->create_category([
383 'name' => 'CAT_B',
386 // Create a new test user.
387 $testuser = self::getDataGenerator()->create_user();
388 $this->setUser($testuser);
390 // Set the capability for CAT_A only.
391 $contextcata = context_coursecat::instance($categorya->id);
392 $roleid = $this->assignUserCapability('moodle/category:manage', $contextcata->id);
394 // Then we move SUBCAT_A parent: CAT_A => CAT_B.
395 $categories = [
397 'id' => $categoryasub->id,
398 'parent' => $categoryb->id
402 $this->expectException('required_capability_exception');
403 core_course_external::update_categories($categories);
407 * Test create_courses numsections
409 public function test_create_course_numsections() {
410 global $DB;
412 $this->resetAfterTest(true);
414 // Set the required capabilities by the external function.
415 $contextid = context_system::instance()->id;
416 $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
417 $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
419 $numsections = 10;
420 $category = self::getDataGenerator()->create_category();
422 // Create base categories.
423 $course1['fullname'] = 'Test course 1';
424 $course1['shortname'] = 'Testcourse1';
425 $course1['categoryid'] = $category->id;
426 $course1['courseformatoptions'][] = array('name' => 'numsections', 'value' => $numsections);
428 $courses = array($course1);
430 $createdcourses = core_course_external::create_courses($courses);
431 foreach ($createdcourses as $createdcourse) {
432 $existingsections = $DB->get_records('course_sections', array('course' => $createdcourse['id']));
433 $modinfo = get_fast_modinfo($createdcourse['id']);
434 $sections = $modinfo->get_section_info_all();
435 $this->assertEquals(count($sections), $numsections + 1); // Includes generic section.
436 $this->assertEquals(count($existingsections), $numsections + 1); // Includes generic section.
441 * Test create_courses
443 public function test_create_courses() {
444 global $DB;
446 $this->resetAfterTest(true);
448 // Enable course completion.
449 set_config('enablecompletion', 1);
450 // Enable course themes.
451 set_config('allowcoursethemes', 1);
453 // Custom fields.
454 $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
456 $fieldtext = self::getDataGenerator()->create_custom_field([
457 'categoryid' => $fieldcategory->get('id'), 'name' => 'Text', 'shortname' => 'text', 'type' => 'text',
459 $fieldtextarea = self::getDataGenerator()->create_custom_field([
460 'categoryid' => $fieldcategory->get('id'), 'name' => 'Textarea', 'shortname' => 'textarea', 'type' => 'textarea',
463 // Set the required capabilities by the external function
464 $contextid = context_system::instance()->id;
465 $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
466 $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
467 $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
469 $category = self::getDataGenerator()->create_category();
471 // Create base categories.
472 $course1['fullname'] = 'Test course 1';
473 $course1['shortname'] = 'Testcourse1';
474 $course1['categoryid'] = $category->id;
475 $course2['fullname'] = 'Test course 2';
476 $course2['shortname'] = 'Testcourse2';
477 $course2['categoryid'] = $category->id;
478 $course2['idnumber'] = 'testcourse2idnumber';
479 $course2['summary'] = 'Description for course 2';
480 $course2['summaryformat'] = FORMAT_MOODLE;
481 $course2['format'] = 'weeks';
482 $course2['showgrades'] = 1;
483 $course2['newsitems'] = 3;
484 $course2['startdate'] = 1420092000; // 01/01/2015.
485 $course2['enddate'] = 1422669600; // 01/31/2015.
486 $course2['numsections'] = 4;
487 $course2['maxbytes'] = 100000;
488 $course2['showreports'] = 1;
489 $course2['visible'] = 0;
490 $course2['hiddensections'] = 0;
491 $course2['groupmode'] = 0;
492 $course2['groupmodeforce'] = 0;
493 $course2['defaultgroupingid'] = 0;
494 $course2['enablecompletion'] = 1;
495 $course2['completionnotify'] = 1;
496 $course2['lang'] = 'en';
497 $course2['forcetheme'] = 'classic';
498 $course2['courseformatoptions'][] = array('name' => 'automaticenddate', 'value' => 0);
499 $course3['fullname'] = 'Test course 3';
500 $course3['shortname'] = 'Testcourse3';
501 $course3['categoryid'] = $category->id;
502 $course3['format'] = 'topics';
503 $course3options = array('numsections' => 8,
504 'hiddensections' => 1,
505 'coursedisplay' => 1);
506 $course3['courseformatoptions'] = array();
507 foreach ($course3options as $key => $value) {
508 $course3['courseformatoptions'][] = array('name' => $key, 'value' => $value);
510 $course4['fullname'] = 'Test course with custom fields';
511 $course4['shortname'] = 'Testcoursecustomfields';
512 $course4['categoryid'] = $category->id;
513 $course4['customfields'] = [
514 ['shortname' => $fieldtext->get('shortname'), 'value' => 'And I want to tell you so much'],
515 ['shortname' => $fieldtextarea->get('shortname'), 'value' => 'I love you'],
517 $courses = array($course4, $course1, $course2, $course3);
519 $createdcourses = core_course_external::create_courses($courses);
521 // We need to execute the return values cleaning process to simulate the web service server.
522 $createdcourses = external_api::clean_returnvalue(core_course_external::create_courses_returns(), $createdcourses);
524 // Check that right number of courses were created.
525 $this->assertEquals(4, count($createdcourses));
527 // Check that the courses were correctly created.
528 foreach ($createdcourses as $createdcourse) {
529 $courseinfo = course_get_format($createdcourse['id'])->get_course();
531 if ($createdcourse['shortname'] == $course2['shortname']) {
532 $this->assertEquals($courseinfo->fullname, $course2['fullname']);
533 $this->assertEquals($courseinfo->shortname, $course2['shortname']);
534 $this->assertEquals($courseinfo->category, $course2['categoryid']);
535 $this->assertEquals($courseinfo->idnumber, $course2['idnumber']);
536 $this->assertEquals($courseinfo->summary, $course2['summary']);
537 $this->assertEquals($courseinfo->summaryformat, $course2['summaryformat']);
538 $this->assertEquals($courseinfo->format, $course2['format']);
539 $this->assertEquals($courseinfo->showgrades, $course2['showgrades']);
540 $this->assertEquals($courseinfo->newsitems, $course2['newsitems']);
541 $this->assertEquals($courseinfo->startdate, $course2['startdate']);
542 $this->assertEquals($courseinfo->enddate, $course2['enddate']);
543 $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(), $course2['numsections']);
544 $this->assertEquals($courseinfo->maxbytes, $course2['maxbytes']);
545 $this->assertEquals($courseinfo->showreports, $course2['showreports']);
546 $this->assertEquals($courseinfo->visible, $course2['visible']);
547 $this->assertEquals($courseinfo->hiddensections, $course2['hiddensections']);
548 $this->assertEquals($courseinfo->groupmode, $course2['groupmode']);
549 $this->assertEquals($courseinfo->groupmodeforce, $course2['groupmodeforce']);
550 $this->assertEquals($courseinfo->defaultgroupingid, $course2['defaultgroupingid']);
551 $this->assertEquals($courseinfo->completionnotify, $course2['completionnotify']);
552 $this->assertEquals($courseinfo->lang, $course2['lang']);
553 $this->assertEquals($courseinfo->theme, $course2['forcetheme']);
555 // We enabled completion at the beginning of the test.
556 $this->assertEquals($courseinfo->enablecompletion, $course2['enablecompletion']);
558 } else if ($createdcourse['shortname'] == $course1['shortname']) {
559 $courseconfig = get_config('moodlecourse');
560 $this->assertEquals($courseinfo->fullname, $course1['fullname']);
561 $this->assertEquals($courseinfo->shortname, $course1['shortname']);
562 $this->assertEquals($courseinfo->category, $course1['categoryid']);
563 $this->assertEquals($courseinfo->summaryformat, FORMAT_HTML);
564 $this->assertEquals($courseinfo->format, $courseconfig->format);
565 $this->assertEquals($courseinfo->showgrades, $courseconfig->showgrades);
566 $this->assertEquals($courseinfo->newsitems, $courseconfig->newsitems);
567 $this->assertEquals($courseinfo->maxbytes, $courseconfig->maxbytes);
568 $this->assertEquals($courseinfo->showreports, $courseconfig->showreports);
569 $this->assertEquals($courseinfo->groupmode, $courseconfig->groupmode);
570 $this->assertEquals($courseinfo->groupmodeforce, $courseconfig->groupmodeforce);
571 $this->assertEquals($courseinfo->defaultgroupingid, 0);
572 } else if ($createdcourse['shortname'] == $course3['shortname']) {
573 $this->assertEquals($courseinfo->fullname, $course3['fullname']);
574 $this->assertEquals($courseinfo->shortname, $course3['shortname']);
575 $this->assertEquals($courseinfo->category, $course3['categoryid']);
576 $this->assertEquals($courseinfo->format, $course3['format']);
577 $this->assertEquals($courseinfo->hiddensections, $course3options['hiddensections']);
578 $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(),
579 $course3options['numsections']);
580 $this->assertEquals($courseinfo->coursedisplay, $course3options['coursedisplay']);
581 } else if ($createdcourse['shortname'] == $course4['shortname']) {
582 $this->assertEquals($courseinfo->fullname, $course4['fullname']);
583 $this->assertEquals($courseinfo->shortname, $course4['shortname']);
584 $this->assertEquals($courseinfo->category, $course4['categoryid']);
586 $handler = core_course\customfield\course_handler::create();
587 $customfields = $handler->export_instance_data_object($createdcourse['id']);
588 $this->assertEquals((object) [
589 'text' => 'And I want to tell you so much',
590 'textarea' => '<div class="text_to_html">I love you</div>',
591 ], $customfields);
592 } else {
593 throw new moodle_exception('Unexpected shortname');
597 // Call without required capability
598 $this->unassignUserCapability('moodle/course:create', $contextid, $roleid);
599 $this->expectException('required_capability_exception');
600 $createdsubcats = core_course_external::create_courses($courses);
604 * Data provider for testing empty fields produce expected exceptions
606 * @see test_create_courses_empty_field
607 * @see test_update_courses_empty_field
609 * @return array
611 public function course_empty_field_provider(): array {
612 return [
614 'fullname' => '',
615 'shortname' => 'ws101',
616 ], 'fullname'],
618 'fullname' => ' ',
619 'shortname' => 'ws101',
620 ], 'fullname'],
622 'fullname' => 'Web Services',
623 'shortname' => '',
624 ], 'shortname'],
626 'fullname' => 'Web Services',
627 'shortname' => ' ',
628 ], 'shortname'],
633 * Test creating courses with empty fields throws an exception
635 * @param array $course
636 * @param string $expectedemptyfield
638 * @dataProvider course_empty_field_provider
640 public function test_create_courses_empty_field(array $course, string $expectedemptyfield): void {
641 $this->resetAfterTest();
642 $this->setAdminUser();
644 // Create a category for the new course.
645 $course['categoryid'] = $this->getDataGenerator()->create_category()->id;
647 $this->expectException(moodle_exception::class);
648 $this->expectExceptionMessageMatches("/{$expectedemptyfield}/");
649 core_course_external::create_courses([$course]);
653 * Test updating courses with empty fields returns warnings
655 * @param array $course
656 * @param string $expectedemptyfield
658 * @dataProvider course_empty_field_provider
660 public function test_update_courses_empty_field(array $course, string $expectedemptyfield): void {
661 $this->resetAfterTest();
662 $this->setAdminUser();
664 // Create a course to update.
665 $course['id'] = $this->getDataGenerator()->create_course()->id;
667 $result = core_course_external::update_courses([$course]);
668 $result = core_course_external::clean_returnvalue(core_course_external::update_courses_returns(), $result);
670 $this->assertCount(1, $result['warnings']);
672 $warning = reset($result['warnings']);
673 $this->assertEquals('errorinvalidparam', $warning['warningcode']);
674 $this->assertStringContainsString($expectedemptyfield, $warning['message']);
678 * Test delete_courses
680 public function test_delete_courses() {
681 global $DB, $USER;
683 $this->resetAfterTest(true);
685 // Admin can delete a course.
686 $this->setAdminUser();
687 // Validate_context() will fail as the email is not set by $this->setAdminUser().
688 $USER->email = 'emailtopass@example.com';
690 $course1 = self::getDataGenerator()->create_course();
691 $course2 = self::getDataGenerator()->create_course();
692 $course3 = self::getDataGenerator()->create_course();
694 // Delete courses.
695 $result = core_course_external::delete_courses(array($course1->id, $course2->id));
696 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
697 // Check for 0 warnings.
698 $this->assertEquals(0, count($result['warnings']));
700 // Check $course 1 and 2 are deleted.
701 $notdeletedcount = $DB->count_records_select('course',
702 'id IN ( ' . $course1->id . ',' . $course2->id . ')');
703 $this->assertEquals(0, $notdeletedcount);
705 // Try to delete non-existent course.
706 $result = core_course_external::delete_courses(array($course1->id));
707 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
708 // Check for 1 warnings.
709 $this->assertEquals(1, count($result['warnings']));
711 // Try to delete Frontpage course.
712 $result = core_course_external::delete_courses(array(0));
713 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
714 // Check for 1 warnings.
715 $this->assertEquals(1, count($result['warnings']));
717 // Fail when the user has access to course (enrolled) but does not have permission or is not admin.
718 $student1 = self::getDataGenerator()->create_user();
719 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
720 $this->getDataGenerator()->enrol_user($student1->id,
721 $course3->id,
722 $studentrole->id);
723 $this->setUser($student1);
724 $result = core_course_external::delete_courses(array($course3->id));
725 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
726 // Check for 1 warnings.
727 $this->assertEquals(1, count($result['warnings']));
729 // Fail when the user is not allow to access the course (enrolled) or is not admin.
730 $this->setGuestUser();
731 $this->expectException('require_login_exception');
733 $result = core_course_external::delete_courses(array($course3->id));
734 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
738 * Test get_courses
740 public function test_get_courses() {
741 global $DB;
743 $this->resetAfterTest(true);
745 $generatedcourses = array();
746 $coursedata['idnumber'] = 'idnumbercourse1';
747 // Adding tags here to check that format_string is applied.
748 $coursedata['fullname'] = '<b>Course 1 for PHPunit test</b>';
749 $coursedata['shortname'] = '<b>Course 1 for PHPunit test</b>';
750 $coursedata['summary'] = 'Course 1 description';
751 $coursedata['summaryformat'] = FORMAT_MOODLE;
752 $course1 = self::getDataGenerator()->create_course($coursedata);
754 $fieldcategory = self::getDataGenerator()->create_custom_field_category(
755 ['name' => 'Other fields']);
757 $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
758 'categoryid' => $fieldcategory->get('id')];
759 $field = self::getDataGenerator()->create_custom_field($customfield);
761 $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
763 $generatedcourses[$course1->id] = $course1;
764 $course2 = self::getDataGenerator()->create_course();
765 $generatedcourses[$course2->id] = $course2;
766 $course3 = self::getDataGenerator()->create_course(array('format' => 'topics'));
767 $generatedcourses[$course3->id] = $course3;
768 $course4 = self::getDataGenerator()->create_course(['customfields' => [$customfieldvalue]]);
769 $generatedcourses[$course4->id] = $course4;
771 // Set the required capabilities by the external function.
772 $context = context_system::instance();
773 $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
774 $this->assignUserCapability('moodle/course:update',
775 context_course::instance($course1->id)->id, $roleid);
776 $this->assignUserCapability('moodle/course:update',
777 context_course::instance($course2->id)->id, $roleid);
778 $this->assignUserCapability('moodle/course:update',
779 context_course::instance($course3->id)->id, $roleid);
780 $this->assignUserCapability('moodle/course:update',
781 context_course::instance($course4->id)->id, $roleid);
783 $courses = core_course_external::get_courses(array('ids' =>
784 array($course1->id, $course2->id, $course4->id)));
786 // We need to execute the return values cleaning process to simulate the web service server.
787 $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
789 // Check we retrieve the good total number of courses.
790 $this->assertEquals(3, count($courses));
792 foreach ($courses as $course) {
793 $coursecontext = context_course::instance($course['id']);
794 $dbcourse = $generatedcourses[$course['id']];
795 $this->assertEquals($course['idnumber'], $dbcourse->idnumber);
796 $this->assertEquals(
797 $course['fullname'],
798 \core_external\util::format_string($dbcourse->fullname, $coursecontext->id)
800 $this->assertEquals(
801 $course['displayname'],
802 \core_external\util::format_string(get_course_display_name_for_list($dbcourse), $coursecontext->id)
804 // Summary was converted to the HTML format.
805 $this->assertEquals($course['summary'], format_text($dbcourse->summary, FORMAT_MOODLE, array('para' => false)));
806 $this->assertEquals($course['summaryformat'], FORMAT_HTML);
807 $this->assertEquals($course['shortname'], \core_external\util::format_string($dbcourse->shortname, $coursecontext->id));
808 $this->assertEquals($course['categoryid'], $dbcourse->category);
809 $this->assertEquals($course['format'], $dbcourse->format);
810 $this->assertEquals($course['showgrades'], $dbcourse->showgrades);
811 $this->assertEquals($course['newsitems'], $dbcourse->newsitems);
812 $this->assertEquals($course['startdate'], $dbcourse->startdate);
813 $this->assertEquals($course['enddate'], $dbcourse->enddate);
814 $this->assertEquals($course['numsections'], course_get_format($dbcourse)->get_last_section_number());
815 $this->assertEquals($course['maxbytes'], $dbcourse->maxbytes);
816 $this->assertEquals($course['showreports'], $dbcourse->showreports);
817 $this->assertEquals($course['visible'], $dbcourse->visible);
818 $this->assertEquals($course['hiddensections'], $dbcourse->hiddensections);
819 $this->assertEquals($course['groupmode'], $dbcourse->groupmode);
820 $this->assertEquals($course['groupmodeforce'], $dbcourse->groupmodeforce);
821 $this->assertEquals($course['defaultgroupingid'], $dbcourse->defaultgroupingid);
822 $this->assertEquals($course['completionnotify'], $dbcourse->completionnotify);
823 $this->assertEquals($course['lang'], $dbcourse->lang);
824 $this->assertEquals($course['forcetheme'], $dbcourse->theme);
825 $this->assertEquals($course['enablecompletion'], $dbcourse->enablecompletion);
826 if ($dbcourse->format === 'topics') {
827 $this->assertEquals($course['courseformatoptions'], array(
828 array('name' => 'hiddensections', 'value' => $dbcourse->hiddensections),
829 array('name' => 'coursedisplay', 'value' => $dbcourse->coursedisplay),
833 // Assert custom field that we previously added to test course 4.
834 if ($dbcourse->id == $course4->id) {
835 $this->assertEquals([
836 'shortname' => $customfield['shortname'],
837 'name' => $customfield['name'],
838 'type' => $customfield['type'],
839 'value' => $customfieldvalue['value'],
840 'valueraw' => $customfieldvalue['value'],
841 ], $course['customfields'][0]);
845 // Get all courses in the DB
846 $courses = core_course_external::get_courses(array());
848 // We need to execute the return values cleaning process to simulate the web service server.
849 $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
851 $this->assertEquals($DB->count_records('course'), count($courses));
855 * Test retrieving courses returns custom field data
857 public function test_get_courses_customfields(): void {
858 $this->resetAfterTest();
859 $this->setAdminUser();
861 $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
862 $datefield = $this->getDataGenerator()->create_custom_field([
863 'categoryid' => $fieldcategory->get('id'),
864 'shortname' => 'mydate',
865 'name' => 'My date',
866 'type' => 'date',
869 $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
871 'shortname' => $datefield->get('shortname'),
872 'value' => 1580389200, // 30/01/2020 13:00 GMT.
874 ]]);
876 $courses = external_api::clean_returnvalue(
877 core_course_external::get_courses_returns(),
878 core_course_external::get_courses(['ids' => [$newcourse->id]])
881 $this->assertCount(1, $courses);
882 $course = reset($courses);
884 $this->assertArrayHasKey('customfields', $course);
885 $this->assertCount(1, $course['customfields']);
887 // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
888 $this->assertEquals([
889 'name' => $datefield->get('name'),
890 'shortname' => $datefield->get('shortname'),
891 'type' => $datefield->get('type'),
892 'value' => userdate(1580389200),
893 'valueraw' => 1580389200,
894 ], reset($course['customfields']));
898 * Test get_courses without capability
900 public function test_get_courses_without_capability() {
901 $this->resetAfterTest(true);
903 $course1 = $this->getDataGenerator()->create_course();
904 $this->setUser($this->getDataGenerator()->create_user());
906 // No permissions are required to get the site course.
907 $courses = core_course_external::get_courses(array('ids' => [SITEID]));
908 $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
910 $this->assertEquals(1, count($courses));
911 $this->assertEquals('PHPUnit test site', $courses[0]['fullname']);
912 $this->assertEquals('site', $courses[0]['format']);
914 // Requesting course without being enrolled or capability to view it will throw an exception.
915 try {
916 core_course_external::get_courses(array('ids' => [$course1->id]));
917 $this->fail('Exception expected');
918 } catch (moodle_exception $e) {
919 $this->assertEquals(1, preg_match('/Course or activity not accessible. \(Not enrolled\)/', $e->getMessage()));
924 * Test search_courses
926 public function test_search_courses() {
928 global $DB;
930 $this->resetAfterTest(true);
931 $this->setAdminUser();
932 $generatedcourses = array();
933 $coursedata1['fullname'] = 'FIRST COURSE';
934 $course1 = self::getDataGenerator()->create_course($coursedata1);
936 $page = new moodle_page();
937 $page->set_course($course1);
938 $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
940 $coursedata2['fullname'] = 'SECOND COURSE';
941 $course2 = self::getDataGenerator()->create_course($coursedata2);
943 $page = new moodle_page();
944 $page->set_course($course2);
945 $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
947 // Search by name.
948 $results = core_course_external::search_courses('search', 'FIRST');
949 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
950 $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
951 $this->assertCount(1, $results['courses']);
953 // Create the forum.
954 $record = new stdClass();
955 $record->introformat = FORMAT_HTML;
956 $record->course = $course2->id;
957 // Set Aggregate type = Average of ratings.
958 $forum = self::getDataGenerator()->create_module('forum', $record);
960 // Search by module.
961 $results = core_course_external::search_courses('modulelist', 'forum');
962 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
963 $this->assertEquals(1, $results['total']);
965 // Enable coursetag option.
966 set_config('block_tags_showcoursetags', true);
967 // Add tag 'TAG-LABEL ON SECOND COURSE' to Course2.
968 core_tag_tag::set_item_tags('core', 'course', $course2->id, context_course::instance($course2->id),
969 array('TAG-LABEL ON SECOND COURSE'));
970 $taginstance = $DB->get_record('tag_instance',
971 array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST);
973 // Search by tagid.
974 $results = core_course_external::search_courses('tagid', $taginstance->tagid);
975 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
976 $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
978 // Search by block (use news_items default block).
979 $blockid = $DB->get_field('block', 'id', array('name' => 'news_items'));
980 $results = core_course_external::search_courses('blocklist', $blockid);
981 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
982 $this->assertEquals(2, $results['total']);
984 // Now as a normal user.
985 $user = self::getDataGenerator()->create_user();
987 // Add a 3rd, hidden, course we shouldn't see, even when enrolled as student.
988 $coursedata3['fullname'] = 'HIDDEN COURSE';
989 $coursedata3['visible'] = 0;
990 $course3 = self::getDataGenerator()->create_course($coursedata3);
991 $this->getDataGenerator()->enrol_user($user->id, $course3->id, 'student');
993 $this->getDataGenerator()->enrol_user($user->id, $course2->id, 'student');
994 $this->setUser($user);
996 $results = core_course_external::search_courses('search', 'FIRST');
997 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
998 $this->assertCount(1, $results['courses']);
999 $this->assertEquals(1, $results['total']);
1000 $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
1002 // Check that we can see all courses without the limit to enrolled setting.
1003 $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 0);
1004 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
1005 $this->assertCount(2, $results['courses']);
1006 $this->assertEquals(2, $results['total']);
1008 // Check that we only see our enrolled course when limiting.
1009 $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 1);
1010 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
1011 $this->assertCount(1, $results['courses']);
1012 $this->assertEquals(1, $results['total']);
1013 $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
1015 // Search by block (use news_items default block). Should fail (only admins allowed).
1016 $this->expectException('required_capability_exception');
1017 $results = core_course_external::search_courses('blocklist', $blockid);
1021 * Test searching for courses returns custom field data
1023 public function test_search_courses_customfields(): void {
1024 $this->resetAfterTest();
1025 $this->setAdminUser();
1027 $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
1028 $datefield = $this->getDataGenerator()->create_custom_field([
1029 'categoryid' => $fieldcategory->get('id'),
1030 'shortname' => 'mydate',
1031 'name' => 'My date',
1032 'type' => 'date',
1035 $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
1037 'shortname' => $datefield->get('shortname'),
1038 'value' => 1580389200, // 30/01/2020 13:00 GMT.
1040 ]]);
1042 $result = external_api::clean_returnvalue(
1043 core_course_external::search_courses_returns(),
1044 core_course_external::search_courses('search', $newcourse->shortname)
1047 $this->assertCount(1, $result['courses']);
1048 $course = reset($result['courses']);
1050 $this->assertArrayHasKey('customfields', $course);
1051 $this->assertCount(1, $course['customfields']);
1053 // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
1054 $this->assertEquals([
1055 'name' => $datefield->get('name'),
1056 'shortname' => $datefield->get('shortname'),
1057 'type' => $datefield->get('type'),
1058 'value' => userdate(1580389200),
1059 'valueraw' => 1580389200,
1060 ], reset($course['customfields']));
1064 * Create a course with contents
1065 * @return array A list with the course object and course modules objects
1067 private function prepare_get_course_contents_test() {
1068 global $DB, $CFG;
1070 $CFG->allowstealth = 1; // Allow stealth activities.
1071 $CFG->enablecompletion = true;
1072 // Course with 4 sections (apart from the main section), with completion and not displaying hidden sections.
1073 $course = self::getDataGenerator()->create_course(['numsections' => 4, 'enablecompletion' => 1, 'hiddensections' => 1]);
1075 $forumdescription = 'This is the forum description';
1076 $forum = $this->getDataGenerator()->create_module('forum',
1077 array('course' => $course->id, 'intro' => $forumdescription, 'trackingtype' => 2),
1078 array('showdescription' => true, 'completion' => COMPLETION_TRACKING_MANUAL));
1079 $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1080 // Add discussions to the tracking forced forum.
1081 $record = new stdClass();
1082 $record->course = $course->id;
1083 $record->userid = 0;
1084 $record->forum = $forum->id;
1085 $discussionforce = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1086 $data = $this->getDataGenerator()->create_module('data',
1087 array('assessed' => 1, 'scale' => 100, 'course' => $course->id, 'completion' => 2, 'completionentries' => 3));
1088 $datacm = get_coursemodule_from_instance('data', $data->id);
1089 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
1090 $pagecm = get_coursemodule_from_instance('page', $page->id);
1091 // This is an stealth page (set by visibleoncoursepage).
1092 $pagestealth = $this->getDataGenerator()->create_module('page', array('course' => $course->id, 'visibleoncoursepage' => 0));
1093 $labeldescription = 'This is a very long label to test if more than 50 characters are returned.
1094 So bla bla bla bla <b>bold bold bold</b> bla bla bla bla.';
1095 $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
1096 'intro' => $labeldescription, 'completion' => COMPLETION_TRACKING_MANUAL));
1097 $labelcm = get_coursemodule_from_instance('label', $label->id);
1098 $tomorrow = time() + DAYSECS;
1099 // Module with availability restrictions not met.
1100 $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '},'
1101 .'{"type":"completion","cm":' . $label->cmid .',"e":1}],"showc":[true,true]}';
1102 $url = $this->getDataGenerator()->create_module('url',
1103 array('course' => $course->id, 'name' => 'URL: % & $ ../', 'section' => 2, 'display' => RESOURCELIB_DISPLAY_POPUP,
1104 'popupwidth' => 100, 'popupheight' => 100),
1105 array('availability' => $availability));
1106 $urlcm = get_coursemodule_from_instance('url', $url->id);
1107 // Module for the last section.
1108 $this->getDataGenerator()->create_module('url',
1109 array('course' => $course->id, 'name' => 'URL for last section', 'section' => 3));
1110 // Module for section 1 with availability restrictions met.
1111 $yesterday = time() - DAYSECS;
1112 $this->getDataGenerator()->create_module('url',
1113 array('course' => $course->id, 'name' => 'URL restrictions met', 'section' => 1),
1114 array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":'. $yesterday .'}],"showc":[true]}'));
1116 // Set the required capabilities by the external function.
1117 $context = context_course::instance($course->id);
1118 $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
1119 $this->assignUserCapability('moodle/course:update', $context->id, $roleid);
1120 $this->assignUserCapability('mod/data:view', $context->id, $roleid);
1122 $conditions = array('course' => $course->id, 'section' => 2);
1123 $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
1125 // Add date availability condition not met for section 3.
1126 $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}';
1127 $DB->set_field('course_sections', 'availability', $availability,
1128 array('course' => $course->id, 'section' => 3));
1130 // Create resource for last section.
1131 $pageinhiddensection = $this->getDataGenerator()->create_module('page',
1132 array('course' => $course->id, 'name' => 'Page in hidden section', 'section' => 4));
1133 // Set not visible last section.
1134 $DB->set_field('course_sections', 'visible', 0,
1135 array('course' => $course->id, 'section' => 4));
1137 $forumcompleteauto = $this->getDataGenerator()->create_module('forum',
1138 array('course' => $course->id, 'intro' => 'forum completion tracking auto', 'trackingtype' => 2),
1139 array('showdescription' => true, 'completionview' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC));
1140 $forumcompleteautocm = get_coursemodule_from_id('forum', $forumcompleteauto->cmid);
1141 $sectionrecord = $DB->get_record('course_sections', $conditions);
1142 // Invalidate the section cache by given section number.
1143 course_modinfo::purge_course_section_cache_by_number($sectionrecord->course, $sectionrecord->section);
1144 rebuild_course_cache($course->id, true, true);
1146 return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm);
1150 * Test get_course_contents
1152 public function test_get_course_contents() {
1153 global $CFG;
1154 $this->resetAfterTest(true);
1156 $CFG->forum_allowforcedreadtracking = 1;
1157 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1159 // Create a resource with all the appearance options enabled. By default it's a text file and will be added to section 1.
1160 $record = (object) [
1161 'course' => $course->id,
1162 'showsize' => 1,
1163 'showtype' => 1,
1164 'showdate' => 1,
1166 $resource = self::getDataGenerator()->create_module('resource', $record);
1167 $h5pactivity = self::getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
1169 // We first run the test as admin.
1170 $this->setAdminUser();
1171 $sections = core_course_external::get_course_contents($course->id, array());
1172 // We need to execute the return values cleaning process to simulate the web service server.
1173 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1175 $modinfo = get_fast_modinfo($course);
1176 $testexecuted = 0;
1177 foreach ($sections[0]['modules'] as $module) {
1178 if ($module['id'] == $forumcm->id and $module['modname'] == 'forum') {
1179 $cm = $modinfo->cms[$forumcm->id];
1180 $formattedtext = format_text($cm->content, FORMAT_HTML,
1181 array('noclean' => true, 'para' => false, 'filter' => false));
1182 $this->assertEquals($formattedtext, $module['description']);
1183 $this->assertEquals($forumcm->instance, $module['instance']);
1184 $this->assertEquals(context_module::instance($forumcm->id)->id, $module['contextid']);
1185 $this->assertFalse($module['noviewlink']);
1186 $this->assertNotEmpty($module['description']); // Module showdescription is on.
1187 // Afterlink for forums has been removed; it has been moved to the new activity badge content.
1188 $this->assertEmpty($module['afterlink']);
1189 $this->assertEquals('1 unread post', $module['activitybadge']['badgecontent']);
1190 $this->assertEquals('bg-dark text-white', $module['activitybadge']['badgestyle']);
1191 $this->assertEquals(
1192 plugin_supports(
1193 'mod',
1194 'forum',
1195 FEATURE_MOD_PURPOSE,
1196 MOD_PURPOSE_OTHER
1197 ), $module['purpose']
1199 $this->assertFalse($module['branded']);
1200 $testexecuted = $testexecuted + 2;
1201 } else if ($module['id'] == $labelcm->id and $module['modname'] == 'label') {
1202 $cm = $modinfo->cms[$labelcm->id];
1203 $formattedtext = format_text($cm->content, FORMAT_HTML,
1204 array('noclean' => true, 'para' => false, 'filter' => false));
1205 $this->assertEquals($formattedtext, $module['description']);
1206 $this->assertEquals($labelcm->instance, $module['instance']);
1207 $this->assertEquals(context_module::instance($labelcm->id)->id, $module['contextid']);
1208 $this->assertTrue($module['noviewlink']);
1209 $this->assertNotEmpty($module['description']); // Label always prints the description.
1210 $this->assertEquals(
1211 plugin_supports(
1212 'mod',
1213 'label',
1214 FEATURE_MOD_PURPOSE,
1215 MOD_PURPOSE_OTHER
1216 ), $module['purpose']
1218 $this->assertFalse($module['branded']);
1219 $testexecuted = $testexecuted + 1;
1220 } else if ($module['id'] == $datacm->id and $module['modname'] == 'data') {
1221 $this->assertStringContainsString('customcompletionrules', $module['customdata']);
1222 $this->assertFalse($module['noviewlink']);
1223 $this->assertArrayNotHasKey('description', $module);
1224 $this->assertEquals(
1225 plugin_supports(
1226 'mod',
1227 'data',
1228 FEATURE_MOD_PURPOSE,
1229 MOD_PURPOSE_OTHER
1230 ), $module['purpose']
1232 $this->assertFalse($module['branded']);
1233 $testexecuted = $testexecuted + 1;
1234 } else if ($module['instance'] == $resource->id && $module['modname'] == 'resource') {
1235 // Resources have both, afterlink for the size and the update date and activitybadge for the file type.
1236 $this->assertStringContainsString('32 bytes', $module['afterlink']);
1237 $this->assertEquals('TXT', $module['activitybadge']['badgecontent']);
1238 $this->assertEquals('badge-none', $module['activitybadge']['badgestyle']);
1239 $this->assertEquals(
1240 plugin_supports(
1241 'mod',
1242 'resource',
1243 FEATURE_MOD_PURPOSE,
1244 MOD_PURPOSE_OTHER
1245 ), $module['purpose']
1247 $this->assertFalse($module['branded']);
1248 $testexecuted = $testexecuted + 1;
1249 } else if ($module['instance'] == $h5pactivity->id && $module['modname'] == 'h5pactivity') {
1250 $this->assertEquals(
1251 plugin_supports(
1252 'mod',
1253 'h5pactivity',
1254 FEATURE_MOD_PURPOSE,
1255 MOD_PURPOSE_OTHER
1256 ), $module['purpose']
1258 $this->assertTrue($module['branded']);
1259 $testexecuted = $testexecuted + 1;
1262 foreach ($sections[2]['modules'] as $module) {
1263 if ($module['id'] == $urlcm->id and $module['modname'] == 'url') {
1264 $this->assertStringContainsString('width=100,height=100', $module['onclick']);
1265 $testexecuted = $testexecuted + 1;
1269 $CFG->forum_allowforcedreadtracking = 0; // Recover original value.
1270 forum_tp_count_forum_unread_posts($forumcm, $course, true); // Reset static cache for further tests.
1272 $this->assertEquals(7, $testexecuted);
1273 $this->assertEquals(0, $sections[0]['section']);
1275 $this->assertCount(8, $sections[0]['modules']);
1276 $this->assertCount(1, $sections[1]['modules']);
1277 $this->assertCount(1, $sections[2]['modules']);
1278 $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions.
1279 $this->assertCount(1, $sections[4]['modules']); // One module for the hidden section with a visible activity.
1280 $this->assertNotEmpty($sections[3]['availabilityinfo']);
1281 $this->assertEquals(1, $sections[1]['section']);
1282 $this->assertEquals(2, $sections[2]['section']);
1283 $this->assertEquals(3, $sections[3]['section']);
1284 $this->assertEquals(4, $sections[4]['section']);
1285 $this->assertStringContainsString('<iframe', $sections[2]['summary']);
1286 $this->assertStringContainsString('</iframe>', $sections[2]['summary']);
1287 $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']);
1288 try {
1289 $sections = core_course_external::get_course_contents($course->id,
1290 array(array("name" => "invalid", "value" => 1)));
1291 $this->fail('Exception expected due to invalid option.');
1292 } catch (moodle_exception $e) {
1293 $this->assertEquals('errorinvalidparam', $e->errorcode);
1299 * Test get_course_contents as student
1301 public function test_get_course_contents_student() {
1302 global $DB;
1303 $this->resetAfterTest(true);
1305 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1307 $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
1308 $user = self::getDataGenerator()->create_user();
1309 self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
1310 $this->setUser($user);
1312 $sections = core_course_external::get_course_contents($course->id, array());
1313 // We need to execute the return values cleaning process to simulate the web service server.
1314 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1316 $this->assertCount(4, $sections); // Nothing for the not visible section.
1317 $this->assertCount(6, $sections[0]['modules']);
1318 $this->assertCount(1, $sections[1]['modules']);
1319 $this->assertCount(1, $sections[2]['modules']);
1320 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1322 $this->assertNotEmpty($sections[3]['availabilityinfo']);
1323 $this->assertEquals(1, $sections[1]['section']);
1324 $this->assertEquals(2, $sections[2]['section']);
1325 $this->assertEquals(3, $sections[3]['section']);
1326 // The module with the availability restriction met is returning contents.
1327 $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
1328 // The module with the availability restriction not met is not returning contents.
1329 $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
1331 // Now include flag for returning stealth information (fake section).
1332 $sections = core_course_external::get_course_contents($course->id,
1333 array(array("name" => "includestealthmodules", "value" => 1)));
1334 // We need to execute the return values cleaning process to simulate the web service server.
1335 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1337 $this->assertCount(5, $sections); // Include fake section with stealth activities.
1338 $this->assertCount(6, $sections[0]['modules']);
1339 $this->assertCount(1, $sections[1]['modules']);
1340 $this->assertCount(1, $sections[2]['modules']);
1341 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1342 $this->assertCount(1, $sections[4]['modules']); // One stealth module.
1343 $this->assertEquals(-1, $sections[4]['id']);
1347 * Test get_course_contents excluding modules
1349 public function test_get_course_contents_excluding_modules() {
1350 $this->resetAfterTest(true);
1352 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1354 // Test exclude modules.
1355 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludemodules", "value" => 1)));
1357 // We need to execute the return values cleaning process to simulate the web service server.
1358 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1360 $this->assertEmpty($sections[0]['modules']);
1361 $this->assertEmpty($sections[1]['modules']);
1365 * Test get_course_contents excluding contents
1367 public function test_get_course_contents_excluding_contents() {
1368 $this->resetAfterTest(true);
1370 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1372 // Test exclude modules.
1373 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludecontents", "value" => 1)));
1375 // We need to execute the return values cleaning process to simulate the web service server.
1376 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1378 foreach ($sections as $section) {
1379 foreach ($section['modules'] as $module) {
1380 // Only resources return contents.
1381 if (isset($module['contents'])) {
1382 $this->assertEmpty($module['contents']);
1389 * Test get_course_contents filtering by section number
1391 public function test_get_course_contents_section_number() {
1392 $this->resetAfterTest(true);
1394 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1396 // Test exclude modules.
1397 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "sectionnumber", "value" => 0)));
1399 // We need to execute the return values cleaning process to simulate the web service server.
1400 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1402 $this->assertCount(1, $sections);
1403 $this->assertCount(6, $sections[0]['modules']);
1407 * Test get_course_contents filtering by cmid
1409 public function test_get_course_contents_cmid() {
1410 $this->resetAfterTest(true);
1412 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1414 // Test exclude modules.
1415 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "cmid", "value" => $forumcm->id)));
1417 // We need to execute the return values cleaning process to simulate the web service server.
1418 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1420 $this->assertCount(4, $sections);
1421 $this->assertCount(1, $sections[0]['modules']);
1422 $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1427 * Test get_course_contents filtering by cmid and section
1429 public function test_get_course_contents_section_cmid() {
1430 $this->resetAfterTest(true);
1432 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1434 // Test exclude modules.
1435 $sections = core_course_external::get_course_contents($course->id, array(
1436 array("name" => "cmid", "value" => $forumcm->id),
1437 array("name" => "sectionnumber", "value" => 0)
1440 // We need to execute the return values cleaning process to simulate the web service server.
1441 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1443 $this->assertCount(1, $sections);
1444 $this->assertCount(1, $sections[0]['modules']);
1445 $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1449 * Test get_course_contents filtering by modname
1451 public function test_get_course_contents_modname() {
1452 $this->resetAfterTest(true);
1454 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1456 // Test exclude modules.
1457 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "modname", "value" => "forum")));
1459 // We need to execute the return values cleaning process to simulate the web service server.
1460 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1462 $this->assertCount(4, $sections);
1463 $this->assertCount(2, $sections[0]['modules']);
1464 $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1468 * Test get_course_contents filtering by modname
1470 public function test_get_course_contents_modid() {
1471 $this->resetAfterTest(true);
1473 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1475 // Test exclude modules.
1476 $sections = core_course_external::get_course_contents($course->id, array(
1477 array("name" => "modname", "value" => "page"),
1478 array("name" => "modid", "value" => $pagecm->instance),
1481 // We need to execute the return values cleaning process to simulate the web service server.
1482 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1484 $this->assertCount(4, $sections);
1485 $this->assertCount(1, $sections[0]['modules']);
1486 $this->assertEquals("page", $sections[0]['modules'][0]["modname"]);
1487 $this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]);
1491 * Test get_course_contents returns downloadcontent value.
1493 public function test_get_course_contents_downloadcontent() {
1494 $this->resetAfterTest();
1496 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1498 // Test exclude modules.
1499 $sections = core_course_external::get_course_contents($course->id, [
1500 ['name' => 'modname', 'value' => 'page'],
1501 ['name' => 'modid', 'value' => $pagecm->instance]
1504 // We need to execute the return values cleaning process to simulate the web service server.
1505 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1506 $this->assertCount(1, $sections[0]['modules']);
1507 $this->assertEquals('page', $sections[0]['modules'][0]['modname']);
1508 $this->assertEquals($pagecm->downloadcontent, $sections[0]['modules'][0]['downloadcontent']);
1509 $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $sections[0]['modules'][0]['downloadcontent']);
1513 * Test get course contents completion manual
1515 public function test_get_course_contents_completion_manual() {
1516 global $CFG;
1517 $this->resetAfterTest(true);
1519 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) =
1520 $this->prepare_get_course_contents_test();
1521 availability_completion\condition::wipe_static_cache();
1523 // Test activity not completed yet.
1524 $result = core_course_external::get_course_contents($course->id, array(
1525 array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1526 // We need to execute the return values cleaning process to simulate the web service server.
1527 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1529 $completiondata = $result[0]['modules'][0]["completiondata"];
1530 $this->assertCount(1, $result[0]['modules']);
1531 $this->assertEquals("forum", $result[0]['modules'][0]["modname"]);
1532 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1533 $this->assertEquals(0, $completiondata['state']);
1534 $this->assertEquals(0, $completiondata['timecompleted']);
1535 $this->assertEmpty($completiondata['overrideby']);
1536 $this->assertFalse($completiondata['valueused']);
1537 $this->assertTrue($completiondata['hascompletion']);
1538 $this->assertFalse($completiondata['isautomatic']);
1539 $this->assertFalse($completiondata['istrackeduser']);
1540 $this->assertTrue($completiondata['uservisible']);
1541 $this->assertFalse($completiondata['isoverallcomplete']);
1543 // Set activity completed.
1544 core_completion_external::update_activity_completion_status_manually($forumcm->id, true);
1546 $result = core_course_external::get_course_contents($course->id, array(
1547 array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1548 // We need to execute the return values cleaning process to simulate the web service server.
1549 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1551 $this->assertEquals(COMPLETION_COMPLETE, $result[0]['modules'][0]["completiondata"]['state']);
1552 $this->assertTrue($result[0]['modules'][0]["completiondata"]['isoverallcomplete']);
1553 $this->assertNotEmpty($result[0]['modules'][0]["completiondata"]['timecompleted']);
1554 $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']);
1556 // Test activity with completion value that is used in an availability condition.
1557 $result = core_course_external::get_course_contents($course->id, array(
1558 array("name" => "modname", "value" => "label"), array("name" => "modid", "value" => $labelcm->instance)));
1559 // We need to execute the return values cleaning process to simulate the web service server.
1560 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1562 $completiondata = $result[0]['modules'][0]["completiondata"];
1563 $this->assertCount(1, $result[0]['modules']);
1564 $this->assertEquals("label", $result[0]['modules'][0]["modname"]);
1565 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1566 $this->assertEquals(0, $completiondata['state']);
1567 $this->assertEquals(0, $completiondata['timecompleted']);
1568 $this->assertEmpty($completiondata['overrideby']);
1569 $this->assertTrue($completiondata['valueused']);
1570 $this->assertTrue($completiondata['hascompletion']);
1571 $this->assertFalse($completiondata['isautomatic']);
1572 $this->assertFalse($completiondata['istrackeduser']);
1573 $this->assertTrue($completiondata['uservisible']);
1574 $this->assertFalse($completiondata['isoverallcomplete']);
1576 // Disable completion.
1577 $CFG->enablecompletion = 0;
1578 $result = core_course_external::get_course_contents($course->id, array(
1579 array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1580 // We need to execute the return values cleaning process to simulate the web service server.
1581 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1583 $this->assertArrayNotHasKey('completiondata', $result[0]['modules'][0]);
1587 * Test get course contents completion auto
1589 public function test_get_course_contents_completion_auto() {
1590 global $CFG;
1591 $this->resetAfterTest(true);
1593 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) =
1594 $this->prepare_get_course_contents_test();
1595 availability_completion\condition::wipe_static_cache();
1597 // Test activity not completed yet.
1598 $result = core_course_external::get_course_contents($course->id, [
1600 "name" => "modname",
1601 "value" => "forum"
1604 "name" => "modid",
1605 "value" => $forumcompleteautocm->instance
1608 // We need to execute the return values cleaning process to simulate the web service server.
1609 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1611 $forummod = $result[0]['modules'][0];
1612 $completiondata = $forummod["completiondata"];
1613 $this->assertCount(1, $result[0]['modules']);
1614 $this->assertEquals("forum", $forummod["modname"]);
1615 $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $forummod["completion"]);
1616 $this->assertEquals(0, $completiondata['state']);
1617 $this->assertEquals(0, $completiondata['timecompleted']);
1618 $this->assertEmpty($completiondata['overrideby']);
1619 $this->assertFalse($completiondata['valueused']);
1620 $this->assertTrue($completiondata['hascompletion']);
1621 $this->assertTrue($completiondata['isautomatic']);
1622 $this->assertFalse($completiondata['istrackeduser']);
1623 $this->assertTrue($completiondata['uservisible']);
1624 $this->assertCount(1, $completiondata['details']);
1625 $this->assertFalse($completiondata['isoverallcomplete']);
1629 * Test mimetype is returned for resources with showtype set.
1631 public function test_get_course_contents_including_mimetype() {
1632 $this->resetAfterTest(true);
1634 $this->setAdminUser();
1635 $course = self::getDataGenerator()->create_course();
1637 $record = new stdClass();
1638 $record->course = $course->id;
1639 $record->showtype = 1;
1640 $resource = self::getDataGenerator()->create_module('resource', $record);
1642 $result = core_course_external::get_course_contents($course->id);
1643 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1644 $this->assertCount(1, $result[0]['modules']); // One module, first section.
1645 $customdata = json_decode($result[0]['modules'][0]['customdata']);
1646 $displayoptions = unserialize($customdata->displayoptions);
1647 $this->assertEquals('text/plain', $displayoptions['filedetails']['mimetype']);
1651 * Test contents info is returned.
1653 public function test_get_course_contents_contentsinfo() {
1654 global $USER;
1656 $this->resetAfterTest(true);
1657 $this->setAdminUser();
1658 $timenow = time();
1660 $course = self::getDataGenerator()->create_course();
1662 $record = new stdClass();
1663 $record->course = $course->id;
1664 // One resource with one file.
1665 $resource1 = self::getDataGenerator()->create_module('resource', $record);
1667 // More type of files.
1668 $record->files = file_get_unused_draft_itemid();
1669 $usercontext = context_user::instance($USER->id);
1670 $extensions = array('txt', 'png', 'pdf');
1671 $fs = get_file_storage();
1672 foreach ($extensions as $key => $extension) {
1673 // Add actual file there.
1674 $filerecord = array('component' => 'user', 'filearea' => 'draft',
1675 'contextid' => $usercontext->id, 'itemid' => $record->files,
1676 'filename' => 'resource' . $key . '.' . $extension, 'filepath' => '/');
1677 $fs->create_file_from_string($filerecord, 'Test resource ' . $key . ' file');
1680 // Create file reference.
1681 $repos = repository::get_instances(array('type' => 'user'));
1682 $userrepository = reset($repos);
1684 // Create a user private file.
1685 $userfilerecord = new stdClass;
1686 $userfilerecord->contextid = $usercontext->id;
1687 $userfilerecord->component = 'user';
1688 $userfilerecord->filearea = 'private';
1689 $userfilerecord->itemid = 0;
1690 $userfilerecord->filepath = '/';
1691 $userfilerecord->filename = 'userfile.txt';
1692 $userfilerecord->source = 'test';
1693 $userfile = $fs->create_file_from_string($userfilerecord, 'User file content');
1694 $userfileref = $fs->pack_reference($userfilerecord);
1696 // Clone latest "normal" file.
1697 $filerefrecord = clone (object) $filerecord;
1698 $filerefrecord->filename = 'testref.txt';
1699 $fileref = $fs->create_file_from_reference($filerefrecord, $userrepository->id, $userfileref);
1700 // Set main file pointing to the file reference.
1701 file_set_sortorder($usercontext->id, 'user', 'draft', $record->files, $filerefrecord->filepath,
1702 $filerefrecord->filename, 1);
1704 // Once the reference has been created, create the file resource.
1705 $resource2 = self::getDataGenerator()->create_module('resource', $record);
1707 $result = core_course_external::get_course_contents($course->id);
1708 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1709 $this->assertCount(2, $result[0]['modules']);
1710 foreach ($result[0]['modules'] as $module) {
1711 if ($module['instance'] == $resource1->id) {
1712 $this->assertEquals(1, $module['contentsinfo']['filescount']);
1713 $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
1714 $this->assertEquals($module['contents'][0]['filesize'], $module['contentsinfo']['filessize']);
1715 $this->assertEquals(array('text/plain'), $module['contentsinfo']['mimetypes']);
1716 } else {
1717 $this->assertEquals(count($extensions) + 1, $module['contentsinfo']['filescount']);
1718 $filessize = $module['contents'][0]['filesize'] + $module['contents'][1]['filesize'] +
1719 $module['contents'][2]['filesize'] + $module['contents'][3]['filesize'];
1720 $this->assertEquals($filessize, $module['contentsinfo']['filessize']);
1721 $this->assertEquals('user', $module['contentsinfo']['repositorytype']);
1722 $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
1723 $this->assertEquals(array('text/plain', 'image/png', 'application/pdf'), $module['contentsinfo']['mimetypes']);
1729 * Test get_course_contents when hidden sections are displayed.
1731 public function test_get_course_contents_hiddensections() {
1732 global $DB;
1733 $this->resetAfterTest(true);
1735 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1736 // Force returning hidden sections.
1737 $course->hiddensections = 0;
1738 update_course($course);
1740 $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
1741 $user = self::getDataGenerator()->create_user();
1742 self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
1743 $this->setUser($user);
1745 $sections = core_course_external::get_course_contents($course->id, array());
1746 // We need to execute the return values cleaning process to simulate the web service server.
1747 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1749 $this->assertCount(5, $sections); // All the sections, including the "not visible" one.
1750 $this->assertCount(6, $sections[0]['modules']);
1751 $this->assertCount(1, $sections[1]['modules']);
1752 $this->assertCount(1, $sections[2]['modules']);
1753 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1754 $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
1756 $this->assertNotEmpty($sections[3]['availabilityinfo']);
1757 $this->assertEquals(1, $sections[1]['section']);
1758 $this->assertEquals(2, $sections[2]['section']);
1759 $this->assertEquals(3, $sections[3]['section']);
1760 // The module with the availability restriction met is returning contents.
1761 $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
1762 // The module with the availability restriction not met is not returning contents.
1763 $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
1765 // Now include flag for returning stealth information (fake section).
1766 $sections = core_course_external::get_course_contents($course->id,
1767 array(array("name" => "includestealthmodules", "value" => 1)));
1768 // We need to execute the return values cleaning process to simulate the web service server.
1769 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1771 $this->assertCount(6, $sections); // Include fake section with stealth activities.
1772 $this->assertCount(6, $sections[0]['modules']);
1773 $this->assertCount(1, $sections[1]['modules']);
1774 $this->assertCount(1, $sections[2]['modules']);
1775 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1776 $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
1777 $this->assertCount(1, $sections[5]['modules']); // One stealth module.
1778 $this->assertEquals(-1, $sections[5]['id']);
1782 * Test get course contents dates.
1784 public function test_get_course_contents_dates() {
1785 $this->resetAfterTest(true);
1787 $this->setAdminUser();
1788 set_config('enablecourserelativedates', 1);
1790 // Course with just main section.
1791 $timenow = time();
1792 $course = self::getDataGenerator()->create_course(
1793 ['numsections' => 0, 'relativedatesmode' => true, 'startdate' => $timenow - DAYSECS]);
1795 $teacher = self::getDataGenerator()->create_user();
1796 self::getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
1798 $this->setUser($teacher);
1800 // Create resource (empty dates).
1801 $resource = self::getDataGenerator()->create_module('resource', ['course' => $course->id]);
1802 // Create activities with dates.
1803 $resource = self::getDataGenerator()->create_module('forum', ['course' => $course->id, 'duedate' => $timenow]);
1804 $resource = self::getDataGenerator()->create_module('choice',
1805 ['course' => $course->id, 'timeopen' => $timenow, 'timeclose' => $timenow + DAYSECS]);
1806 $resource = self::getDataGenerator()->create_module('assign',
1807 ['course' => $course->id, 'allowsubmissionsfromdate' => $timenow]);
1809 $result = core_course_external::get_course_contents($course->id);
1810 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1812 foreach ($result[0]['modules'] as $module) {
1813 if ($module['modname'] == 'resource') {
1814 $this->assertEmpty($module['dates']);
1815 } else if ($module['modname'] == 'forum') {
1816 $this->assertCount(1, $module['dates']);
1817 $this->assertEquals('duedate', $module['dates'][0]['dataid']);
1818 $this->assertEquals($timenow, $module['dates'][0]['timestamp']);
1819 } else if ($module['modname'] == 'choice') {
1820 $this->assertCount(2, $module['dates']);
1821 $this->assertEquals('timeopen', $module['dates'][0]['dataid']);
1822 $this->assertEquals($timenow, $module['dates'][0]['timestamp']);
1823 $this->assertEquals('timeclose', $module['dates'][1]['dataid']);
1824 $this->assertEquals($timenow + DAYSECS, $module['dates'][1]['timestamp']);
1825 } else if ($module['modname'] == 'assign') {
1826 $this->assertCount(1, $module['dates']);
1827 $this->assertEquals('allowsubmissionsfromdate', $module['dates'][0]['dataid']);
1828 $this->assertEquals($timenow, $module['dates'][0]['timestamp']);
1829 $this->assertEquals($course->startdate, $module['dates'][0]['relativeto']);
1835 * Test get_course_contents for courses with invalid course format.
1837 public function test_get_course_contents_invalid_format() {
1838 global $DB;
1839 $this->resetAfterTest();
1841 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1843 $DB->set_field('course', 'format', 'fakeformat', ['id' => $course->id]);
1845 // WS should falback to default course format (topics) and avoid exceptions (but debugging will happen).
1846 $result = core_course_external::get_course_contents($course->id);
1847 $this->assertDebuggingCalled();
1848 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1852 * Test duplicate_course
1854 public function test_duplicate_course() {
1855 $this->resetAfterTest(true);
1857 // Create one course with three modules.
1858 $course = self::getDataGenerator()->create_course();
1859 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
1860 $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1861 $forumcontext = context_module::instance($forum->cmid);
1862 $data = $this->getDataGenerator()->create_module('data', array('assessed'=>1, 'scale'=>100, 'course'=>$course->id));
1863 $datacontext = context_module::instance($data->cmid);
1864 $datacm = get_coursemodule_from_instance('page', $data->id);
1865 $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
1866 $pagecontext = context_module::instance($page->cmid);
1867 $pagecm = get_coursemodule_from_instance('page', $page->id);
1869 // Set the required capabilities by the external function.
1870 $coursecontext = context_course::instance($course->id);
1871 $categorycontext = context_coursecat::instance($course->category);
1872 $roleid = $this->assignUserCapability('moodle/course:create', $categorycontext->id);
1873 $this->assignUserCapability('moodle/course:view', $categorycontext->id, $roleid);
1874 $this->assignUserCapability('moodle/restore:restorecourse', $categorycontext->id, $roleid);
1875 $this->assignUserCapability('moodle/backup:backupcourse', $coursecontext->id, $roleid);
1876 $this->assignUserCapability('moodle/backup:configure', $coursecontext->id, $roleid);
1877 // Optional capabilities to copy user data.
1878 $this->assignUserCapability('moodle/backup:userinfo', $coursecontext->id, $roleid);
1879 $this->assignUserCapability('moodle/restore:userinfo', $categorycontext->id, $roleid);
1881 $newcourse['fullname'] = 'Course duplicate';
1882 $newcourse['shortname'] = 'courseduplicate';
1883 $newcourse['categoryid'] = $course->category;
1884 $newcourse['visible'] = true;
1885 $newcourse['options'][] = array('name' => 'users', 'value' => true);
1887 $duplicate = core_course_external::duplicate_course($course->id, $newcourse['fullname'],
1888 $newcourse['shortname'], $newcourse['categoryid'], $newcourse['visible'], $newcourse['options']);
1890 // We need to execute the return values cleaning process to simulate the web service server.
1891 $duplicate = external_api::clean_returnvalue(core_course_external::duplicate_course_returns(), $duplicate);
1893 // Check that the course has been duplicated.
1894 $this->assertEquals($newcourse['shortname'], $duplicate['shortname']);
1898 * Test update_courses
1900 public function test_update_courses() {
1901 global $DB, $CFG, $USER, $COURSE;
1903 // Get current $COURSE to be able to restore it later (defaults to $SITE). We need this
1904 // trick because we are both updating and getting (for testing) course information
1905 // in the same request and core_course_external::update_courses()
1906 // is overwriting $COURSE all over the time with OLD values, so later
1907 // use of get_course() fetches those OLD values instead of the updated ones.
1908 // See MDL-39723 for more info.
1909 $origcourse = clone($COURSE);
1911 $this->resetAfterTest(true);
1913 // Set the required capabilities by the external function.
1914 $contextid = context_system::instance()->id;
1915 $roleid = $this->assignUserCapability('moodle/course:update', $contextid);
1916 $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1917 $this->assignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
1918 $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1919 $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
1920 $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
1921 $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
1922 $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
1923 $this->assignUserCapability('moodle/course:viewhiddencourses', $contextid, $roleid);
1924 $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
1926 // Create category and courses.
1927 $category1 = self::getDataGenerator()->create_category();
1928 $category2 = self::getDataGenerator()->create_category();
1930 $originalcourse1 = self::getDataGenerator()->create_course();
1931 self::getDataGenerator()->enrol_user($USER->id, $originalcourse1->id, $roleid);
1933 $originalcourse2 = self::getDataGenerator()->create_course();
1934 self::getDataGenerator()->enrol_user($USER->id, $originalcourse2->id, $roleid);
1936 // Course with custom fields.
1937 $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
1939 $fieldtext = self::getDataGenerator()->create_custom_field([
1940 'categoryid' => $fieldcategory->get('id'), 'name' => 'Text', 'shortname' => 'text', 'type' => 'text', 'configdata' => [
1941 'locked' => 1,
1944 $fieldtextarea = self::getDataGenerator()->create_custom_field([
1945 'categoryid' => $fieldcategory->get('id'), 'name' => 'Textarea', 'shortname' => 'textarea', 'type' => 'textarea',
1948 $originalcourse3 = self::getDataGenerator()->create_course();
1949 self::getDataGenerator()->enrol_user($USER->id, $originalcourse3->id, $roleid);
1951 // Course values to be updated.
1952 $course1['id'] = $originalcourse1->id;
1953 $course1['fullname'] = 'Updated test course 1';
1954 $course1['shortname'] = 'Udestedtestcourse1';
1955 $course1['categoryid'] = $category1->id;
1957 $course2['id'] = $originalcourse2->id;
1958 $course2['fullname'] = 'Updated test course 2';
1959 $course2['shortname'] = 'Updestedtestcourse2';
1960 $course2['categoryid'] = $category2->id;
1961 $course2['idnumber'] = 'Updatedidnumber2';
1962 $course2['summary'] = 'Updaated description for course 2';
1963 $course2['summaryformat'] = FORMAT_HTML;
1964 $course2['format'] = 'topics';
1965 $course2['showgrades'] = 1;
1966 $course2['newsitems'] = 3;
1967 $course2['startdate'] = 1420092000; // 01/01/2015.
1968 $course2['enddate'] = 1422669600; // 01/31/2015.
1969 $course2['maxbytes'] = 100000;
1970 $course2['showreports'] = 1;
1971 $course2['visible'] = 0;
1972 $course2['hiddensections'] = 0;
1973 $course2['groupmode'] = 0;
1974 $course2['groupmodeforce'] = 0;
1975 $course2['defaultgroupingid'] = 0;
1976 $course2['enablecompletion'] = 1;
1977 $course2['lang'] = 'en';
1978 $course2['forcetheme'] = 'classic';
1980 $course3['id'] = $originalcourse3->id;
1981 $course3['customfields'] = [
1982 ['shortname' => $fieldtext->get('shortname'), 'value' => 'I long to see the sunlight in your hair'],
1983 ['shortname' => $fieldtextarea->get('shortname'), 'value' => 'And tell you time and time again'],
1986 $courses = array($course1, $course2, $course3);
1988 $updatedcoursewarnings = core_course_external::update_courses($courses);
1989 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1990 $updatedcoursewarnings);
1991 $COURSE = $origcourse; // Restore $COURSE. Instead of using the OLD one set by the previous line.
1993 // Check that right number of courses were created.
1994 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1996 // Check that the courses were correctly created.
1997 foreach ($courses as $course) {
1998 $courseinfo = course_get_format($course['id'])->get_course();
1999 $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course['id']);
2000 if ($course['id'] == $course2['id']) {
2001 $this->assertEquals($course2['fullname'], $courseinfo->fullname);
2002 $this->assertEquals($course2['shortname'], $courseinfo->shortname);
2003 $this->assertEquals($course2['categoryid'], $courseinfo->category);
2004 $this->assertEquals($course2['idnumber'], $courseinfo->idnumber);
2005 $this->assertEquals($course2['summary'], $courseinfo->summary);
2006 $this->assertEquals($course2['summaryformat'], $courseinfo->summaryformat);
2007 $this->assertEquals($course2['format'], $courseinfo->format);
2008 $this->assertEquals($course2['showgrades'], $courseinfo->showgrades);
2009 $this->assertEquals($course2['newsitems'], $courseinfo->newsitems);
2010 $this->assertEquals($course2['startdate'], $courseinfo->startdate);
2011 $this->assertEquals($course2['enddate'], $courseinfo->enddate);
2012 $this->assertEquals($course2['maxbytes'], $courseinfo->maxbytes);
2013 $this->assertEquals($course2['showreports'], $courseinfo->showreports);
2014 $this->assertEquals($course2['visible'], $courseinfo->visible);
2015 $this->assertEquals($course2['hiddensections'], $courseinfo->hiddensections);
2016 $this->assertEquals($course2['groupmode'], $courseinfo->groupmode);
2017 $this->assertEquals($course2['groupmodeforce'], $courseinfo->groupmodeforce);
2018 $this->assertEquals($course2['defaultgroupingid'], $courseinfo->defaultgroupingid);
2019 $this->assertEquals($course2['lang'], $courseinfo->lang);
2021 if (!empty($CFG->allowcoursethemes)) {
2022 $this->assertEquals($course2['forcetheme'], $courseinfo->theme);
2025 $this->assertEquals($course2['enablecompletion'], $courseinfo->enablecompletion);
2026 $this->assertEquals((object) [
2027 'text' => null,
2028 'textarea' => null,
2029 ], $customfields);
2030 } else if ($course['id'] == $course1['id']) {
2031 $this->assertEquals($course1['fullname'], $courseinfo->fullname);
2032 $this->assertEquals($course1['shortname'], $courseinfo->shortname);
2033 $this->assertEquals($course1['categoryid'], $courseinfo->category);
2034 $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
2035 $this->assertEquals('topics', $courseinfo->format);
2036 $this->assertEquals(5, course_get_format($course['id'])->get_last_section_number());
2037 $this->assertEquals(0, $courseinfo->newsitems);
2038 $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
2039 $this->assertEquals((object) [
2040 'text' => null,
2041 'textarea' => null,
2042 ], $customfields);
2043 } else if ($course['id'] == $course3['id']) {
2044 $this->assertEquals((object) [
2045 'text' => 'I long to see the sunlight in your hair',
2046 'textarea' => '<div class="text_to_html">And tell you time and time again</div>',
2047 ], $customfields);
2048 } else {
2049 throw new moodle_exception('Unexpected shortname');
2053 $courses = array($course1);
2054 // Try update course without update capability.
2055 $user = self::getDataGenerator()->create_user();
2056 $this->setUser($user);
2057 $this->unassignUserCapability('moodle/course:update', $contextid, $roleid);
2058 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2059 $updatedcoursewarnings = core_course_external::update_courses($courses);
2060 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2061 $updatedcoursewarnings);
2062 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2064 // Try update course category without capability.
2065 $this->assignUserCapability('moodle/course:update', $contextid, $roleid);
2066 $this->unassignUserCapability('moodle/course:changecategory', $contextid, $roleid);
2067 $user = self::getDataGenerator()->create_user();
2068 $this->setUser($user);
2069 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2070 $course1['categoryid'] = $category2->id;
2071 $courses = array($course1);
2072 $updatedcoursewarnings = core_course_external::update_courses($courses);
2073 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2074 $updatedcoursewarnings);
2075 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2077 // Try update course fullname without capability.
2078 $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
2079 $this->unassignUserCapability('moodle/course:changefullname', $contextid, $roleid);
2080 $user = self::getDataGenerator()->create_user();
2081 $this->setUser($user);
2082 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2083 $updatedcoursewarnings = core_course_external::update_courses($courses);
2084 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2085 $updatedcoursewarnings);
2086 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2087 $course1['fullname'] = 'Testing fullname without permission';
2088 $courses = array($course1);
2089 $updatedcoursewarnings = core_course_external::update_courses($courses);
2090 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2091 $updatedcoursewarnings);
2092 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2094 // Try update course shortname without capability.
2095 $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
2096 $this->unassignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
2097 $user = self::getDataGenerator()->create_user();
2098 $this->setUser($user);
2099 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2100 $updatedcoursewarnings = core_course_external::update_courses($courses);
2101 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2102 $updatedcoursewarnings);
2103 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2104 $course1['shortname'] = 'Testing shortname without permission';
2105 $courses = array($course1);
2106 $updatedcoursewarnings = core_course_external::update_courses($courses);
2107 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2108 $updatedcoursewarnings);
2109 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2111 // Try update course idnumber without capability.
2112 $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
2113 $this->unassignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
2114 $user = self::getDataGenerator()->create_user();
2115 $this->setUser($user);
2116 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2117 $updatedcoursewarnings = core_course_external::update_courses($courses);
2118 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2119 $updatedcoursewarnings);
2120 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2121 $course1['idnumber'] = 'NEWIDNUMBER';
2122 $courses = array($course1);
2123 $updatedcoursewarnings = core_course_external::update_courses($courses);
2124 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2125 $updatedcoursewarnings);
2126 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2128 // Try update course summary without capability.
2129 $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
2130 $this->unassignUserCapability('moodle/course:changesummary', $contextid, $roleid);
2131 $user = self::getDataGenerator()->create_user();
2132 $this->setUser($user);
2133 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2134 $updatedcoursewarnings = core_course_external::update_courses($courses);
2135 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2136 $updatedcoursewarnings);
2137 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2138 $course1['summary'] = 'New summary';
2139 $courses = array($course1);
2140 $updatedcoursewarnings = core_course_external::update_courses($courses);
2141 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2142 $updatedcoursewarnings);
2143 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2145 // Try update course with invalid summary format.
2146 $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
2147 $user = self::getDataGenerator()->create_user();
2148 $this->setUser($user);
2149 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2150 $updatedcoursewarnings = core_course_external::update_courses($courses);
2151 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2152 $updatedcoursewarnings);
2153 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2154 $course1['summaryformat'] = 10;
2155 $courses = array($course1);
2156 $updatedcoursewarnings = core_course_external::update_courses($courses);
2157 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2158 $updatedcoursewarnings);
2159 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2161 // Try update course visibility without capability.
2162 $this->unassignUserCapability('moodle/course:visibility', $contextid, $roleid);
2163 $user = self::getDataGenerator()->create_user();
2164 $this->setUser($user);
2165 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2166 $course1['summaryformat'] = FORMAT_MOODLE;
2167 $courses = array($course1);
2168 $updatedcoursewarnings = core_course_external::update_courses($courses);
2169 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2170 $updatedcoursewarnings);
2171 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2172 $course1['visible'] = 0;
2173 $courses = array($course1);
2174 $updatedcoursewarnings = core_course_external::update_courses($courses);
2175 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2176 $updatedcoursewarnings);
2177 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2179 // Try update course custom fields without capability.
2180 $this->unassignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
2181 $user = self::getDataGenerator()->create_user();
2182 $this->setUser($user);
2183 self::getDataGenerator()->enrol_user($user->id, $course3['id'], $roleid);
2185 $course3['customfields'] = [
2186 ['shortname' => 'text', 'value' => 'New updated value'],
2189 core_course_external::update_courses([$course3]);
2191 // Custom field was not updated.
2192 $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course3['id']);
2193 $this->assertEquals((object) [
2194 'text' => 'I long to see the sunlight in your hair',
2195 'textarea' => '<div class="text_to_html">And tell you time and time again</div>',
2196 ], $customfields);
2200 * Test delete course_module.
2202 public function test_delete_modules() {
2203 global $DB;
2205 // Ensure we reset the data after this test.
2206 $this->resetAfterTest(true);
2208 // Create a user.
2209 $user = self::getDataGenerator()->create_user();
2211 // Set the tests to run as the user.
2212 self::setUser($user);
2214 // Create a course to add the modules.
2215 $course = self::getDataGenerator()->create_course();
2217 // Create two test modules.
2218 $record = new stdClass();
2219 $record->course = $course->id;
2220 $module1 = self::getDataGenerator()->create_module('forum', $record);
2221 $module2 = self::getDataGenerator()->create_module('assign', $record);
2223 // Check the forum was correctly created.
2224 $this->assertEquals(1, $DB->count_records('forum', array('id' => $module1->id)));
2226 // Check the assignment was correctly created.
2227 $this->assertEquals(1, $DB->count_records('assign', array('id' => $module2->id)));
2229 // Check data exists in the course modules table.
2230 $this->assertEquals(2, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
2231 array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
2233 // Enrol the user in the course.
2234 $enrol = enrol_get_plugin('manual');
2235 $enrolinstances = enrol_get_instances($course->id, true);
2236 foreach ($enrolinstances as $courseenrolinstance) {
2237 if ($courseenrolinstance->enrol == "manual") {
2238 $instance = $courseenrolinstance;
2239 break;
2242 $enrol->enrol_user($instance, $user->id);
2244 // Assign capabilities to delete module 1.
2245 $modcontext = context_module::instance($module1->cmid);
2246 $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id);
2248 // Assign capabilities to delete module 2.
2249 $modcontext = context_module::instance($module2->cmid);
2250 $newrole = create_role('Role 2', 'role2', 'Role 2 description');
2251 $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id, $newrole);
2253 // Deleting these module instances.
2254 core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2256 // Check the forum was deleted.
2257 $this->assertEquals(0, $DB->count_records('forum', array('id' => $module1->id)));
2259 // Check the assignment was deleted.
2260 $this->assertEquals(0, $DB->count_records('assign', array('id' => $module2->id)));
2262 // Check we retrieve no data in the course modules table.
2263 $this->assertEquals(0, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
2264 array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
2266 // Call with non-existent course module id and ensure exception thrown.
2267 try {
2268 core_course_external::delete_modules(array('1337'));
2269 $this->fail('Exception expected due to missing course module.');
2270 } catch (dml_missing_record_exception $e) {
2271 $this->assertEquals('invalidcoursemodule', $e->errorcode);
2274 // Create two modules.
2275 $module1 = self::getDataGenerator()->create_module('forum', $record);
2276 $module2 = self::getDataGenerator()->create_module('assign', $record);
2278 // Since these modules were recreated the user will not have capabilities
2279 // to delete them, ensure exception is thrown if they try.
2280 try {
2281 core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2282 $this->fail('Exception expected due to missing capability.');
2283 } catch (moodle_exception $e) {
2284 $this->assertEquals('nopermissions', $e->errorcode);
2287 // Unenrol user from the course.
2288 $enrol->unenrol_user($instance, $user->id);
2290 // Try and delete modules from the course the user was unenrolled in, make sure exception thrown.
2291 try {
2292 core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2293 $this->fail('Exception expected due to being unenrolled from the course.');
2294 } catch (moodle_exception $e) {
2295 $this->assertEquals('requireloginerror', $e->errorcode);
2300 * Test import_course into an empty course
2302 public function test_import_course_empty() {
2303 global $USER;
2305 $this->resetAfterTest(true);
2307 $course1 = self::getDataGenerator()->create_course();
2308 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id, 'name' => 'Forum test'));
2309 $page = $this->getDataGenerator()->create_module('page', array('course' => $course1->id, 'name' => 'Page test'));
2311 $course2 = self::getDataGenerator()->create_course();
2313 $course1cms = get_fast_modinfo($course1->id)->get_cms();
2314 $course2cms = get_fast_modinfo($course2->id)->get_cms();
2316 // Verify the state of the courses before we do the import.
2317 $this->assertCount(2, $course1cms);
2318 $this->assertEmpty($course2cms);
2320 // Setup the user to run the operation (ugly hack because validate_context() will
2321 // fail as the email is not set by $this->setAdminUser()).
2322 $this->setAdminUser();
2323 $USER->email = 'emailtopass@example.com';
2325 // Import from course1 to course2.
2326 core_course_external::import_course($course1->id, $course2->id, 0);
2328 // Verify that now we have two modules in both courses.
2329 $course1cms = get_fast_modinfo($course1->id)->get_cms();
2330 $course2cms = get_fast_modinfo($course2->id)->get_cms();
2331 $this->assertCount(2, $course1cms);
2332 $this->assertCount(2, $course2cms);
2334 // Verify that the names transfered across correctly.
2335 foreach ($course2cms as $cm) {
2336 if ($cm->modname === 'page') {
2337 $this->assertEquals($cm->name, $page->name);
2338 } else if ($cm->modname === 'forum') {
2339 $this->assertEquals($cm->name, $forum->name);
2340 } else {
2341 $this->fail('Unknown CM found.');
2347 * Test import_course into an filled course
2349 public function test_import_course_filled() {
2350 global $USER;
2352 $this->resetAfterTest(true);
2354 // Add forum and page to course1.
2355 $course1 = self::getDataGenerator()->create_course();
2356 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2357 $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
2359 // Add quiz to course 2.
2360 $course2 = self::getDataGenerator()->create_course();
2361 $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
2363 $course1cms = get_fast_modinfo($course1->id)->get_cms();
2364 $course2cms = get_fast_modinfo($course2->id)->get_cms();
2366 // Verify the state of the courses before we do the import.
2367 $this->assertCount(2, $course1cms);
2368 $this->assertCount(1, $course2cms);
2370 // Setup the user to run the operation (ugly hack because validate_context() will
2371 // fail as the email is not set by $this->setAdminUser()).
2372 $this->setAdminUser();
2373 $USER->email = 'emailtopass@example.com';
2375 // Import from course1 to course2 without deleting content.
2376 core_course_external::import_course($course1->id, $course2->id, 0);
2378 $course2cms = get_fast_modinfo($course2->id)->get_cms();
2380 // Verify that now we have three modules in course2.
2381 $this->assertCount(3, $course2cms);
2383 // Verify that the names transfered across correctly.
2384 foreach ($course2cms as $cm) {
2385 if ($cm->modname === 'page') {
2386 $this->assertEquals($cm->name, $page->name);
2387 } else if ($cm->modname === 'forum') {
2388 $this->assertEquals($cm->name, $forum->name);
2389 } else if ($cm->modname === 'quiz') {
2390 $this->assertEquals($cm->name, $quiz->name);
2391 } else {
2392 $this->fail('Unknown CM found.');
2398 * Test import_course with only blocks set to backup
2400 public function test_import_course_blocksonly() {
2401 global $USER, $DB;
2403 $this->resetAfterTest(true);
2405 // Add forum and page to course1.
2406 $course1 = self::getDataGenerator()->create_course();
2407 $course1ctx = context_course::instance($course1->id);
2408 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2409 $block = $this->getDataGenerator()->create_block('online_users', array('parentcontextid' => $course1ctx->id));
2411 $course2 = self::getDataGenerator()->create_course();
2412 $course2ctx = context_course::instance($course2->id);
2413 $initialblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
2414 $initialcmcount = count(get_fast_modinfo($course2->id)->get_cms());
2416 // Setup the user to run the operation (ugly hack because validate_context() will
2417 // fail as the email is not set by $this->setAdminUser()).
2418 $this->setAdminUser();
2419 $USER->email = 'emailtopass@example.com';
2421 // Import from course1 to course2 without deleting content, but excluding
2422 // activities.
2423 $options = array(
2424 array('name' => 'activities', 'value' => 0),
2425 array('name' => 'blocks', 'value' => 1),
2426 array('name' => 'filters', 'value' => 0),
2429 core_course_external::import_course($course1->id, $course2->id, 0, $options);
2431 $newcmcount = count(get_fast_modinfo($course2->id)->get_cms());
2432 $newblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
2433 // Check that course modules haven't changed, but that blocks have.
2434 $this->assertEquals($initialcmcount, $newcmcount);
2435 $this->assertEquals(($initialblockcount + 1), $newblockcount);
2439 * Test import_course into an filled course, deleting content.
2441 public function test_import_course_deletecontent() {
2442 global $USER;
2443 $this->resetAfterTest(true);
2445 // Add forum and page to course1.
2446 $course1 = self::getDataGenerator()->create_course();
2447 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2448 $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
2450 // Add quiz to course 2.
2451 $course2 = self::getDataGenerator()->create_course();
2452 $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
2454 $course1cms = get_fast_modinfo($course1->id)->get_cms();
2455 $course2cms = get_fast_modinfo($course2->id)->get_cms();
2457 // Verify the state of the courses before we do the import.
2458 $this->assertCount(2, $course1cms);
2459 $this->assertCount(1, $course2cms);
2461 // Setup the user to run the operation (ugly hack because validate_context() will
2462 // fail as the email is not set by $this->setAdminUser()).
2463 $this->setAdminUser();
2464 $USER->email = 'emailtopass@example.com';
2466 // Import from course1 to course2, deleting content.
2467 core_course_external::import_course($course1->id, $course2->id, 1);
2469 $course2cms = get_fast_modinfo($course2->id)->get_cms();
2471 // Verify that now we have two modules in course2.
2472 $this->assertCount(2, $course2cms);
2474 // Verify that the course only contains the imported modules.
2475 foreach ($course2cms as $cm) {
2476 if ($cm->modname === 'page') {
2477 $this->assertEquals($cm->name, $page->name);
2478 } else if ($cm->modname === 'forum') {
2479 $this->assertEquals($cm->name, $forum->name);
2480 } else {
2481 $this->fail('Unknown CM found: '.$cm->name);
2487 * Ensure import_course handles incorrect deletecontent option correctly.
2489 public function test_import_course_invalid_deletecontent_option() {
2490 $this->resetAfterTest(true);
2492 $course1 = self::getDataGenerator()->create_course();
2493 $course2 = self::getDataGenerator()->create_course();
2495 $this->expectException('moodle_exception');
2496 $this->expectExceptionMessage(get_string('invalidextparam', 'webservice', -1));
2497 // Import from course1 to course2, with invalid option
2498 core_course_external::import_course($course1->id, $course2->id, -1);;
2502 * Test view_course function
2504 public function test_view_course() {
2506 $this->resetAfterTest();
2508 // Course without sections.
2509 $course = $this->getDataGenerator()->create_course(array('numsections' => 5), array('createsections' => true));
2510 $this->setAdminUser();
2512 // Redirect events to the sink, so we can recover them later.
2513 $sink = $this->redirectEvents();
2515 $result = core_course_external::view_course($course->id, 1);
2516 $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
2517 $events = $sink->get_events();
2518 $event = reset($events);
2520 // Check the event details are correct.
2521 $this->assertInstanceOf('\core\event\course_viewed', $event);
2522 $this->assertEquals(context_course::instance($course->id), $event->get_context());
2523 $this->assertEquals(1, $event->other['coursesectionnumber']);
2525 $result = core_course_external::view_course($course->id);
2526 $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
2527 $events = $sink->get_events();
2528 $event = array_pop($events);
2529 $sink->close();
2531 // Check the event details are correct.
2532 $this->assertInstanceOf('\core\event\course_viewed', $event);
2533 $this->assertEquals(context_course::instance($course->id), $event->get_context());
2534 $this->assertEmpty($event->other);
2539 * Test get_course_module
2541 public function test_get_course_module() {
2542 global $DB;
2544 $this->resetAfterTest(true);
2546 $this->setAdminUser();
2547 $course = self::getDataGenerator()->create_course(['enablecompletion' => 1]);
2548 $record = array(
2549 'course' => $course->id,
2550 'name' => 'First Assignment'
2552 $options = array(
2553 'idnumber' => 'ABC',
2554 'visible' => 0,
2555 'completion' => COMPLETION_TRACKING_AUTOMATIC,
2556 'completiongradeitemnumber' => 0,
2557 'completionpassgrade' => 1,
2559 // Hidden activity.
2560 $assign = self::getDataGenerator()->create_module('assign', $record, $options);
2562 $outcomescale = 'Distinction, Very Good, Good, Pass, Fail';
2564 // Insert a custom grade scale to be used by an outcome.
2565 $gradescale = new grade_scale();
2566 $gradescale->name = 'gettcoursemodulescale';
2567 $gradescale->courseid = $course->id;
2568 $gradescale->userid = 0;
2569 $gradescale->scale = $outcomescale;
2570 $gradescale->description = 'This scale is used to mark standard assignments.';
2571 $gradescale->insert();
2573 // Insert an outcome.
2574 $data = new stdClass();
2575 $data->courseid = $course->id;
2576 $data->fullname = 'Team work';
2577 $data->shortname = 'Team work';
2578 $data->scaleid = $gradescale->id;
2579 $outcome = new grade_outcome($data, false);
2580 $outcome->insert();
2582 $outcomegradeitem = new grade_item();
2583 $outcomegradeitem->itemname = $outcome->shortname;
2584 $outcomegradeitem->itemtype = 'mod';
2585 $outcomegradeitem->itemmodule = 'assign';
2586 $outcomegradeitem->iteminstance = $assign->id;
2587 $outcomegradeitem->outcomeid = $outcome->id;
2588 $outcomegradeitem->cmid = 0;
2589 $outcomegradeitem->courseid = $course->id;
2590 $outcomegradeitem->aggregationcoef = 0;
2591 $outcomegradeitem->itemnumber = 1000; // Outcomes start at 1000.
2592 $outcomegradeitem->gradetype = GRADE_TYPE_SCALE;
2593 $outcomegradeitem->scaleid = $outcome->scaleid;
2594 $outcomegradeitem->insert();
2596 $assignmentgradeitem = grade_item::fetch(
2597 array(
2598 'itemtype' => 'mod',
2599 'itemmodule' => 'assign',
2600 'iteminstance' => $assign->id,
2601 'itemnumber' => 0,
2602 'courseid' => $course->id
2605 $outcomegradeitem->set_parent($assignmentgradeitem->categoryid);
2606 $outcomegradeitem->move_after_sortorder($assignmentgradeitem->sortorder);
2608 // Test admin user can see the complete hidden activity.
2609 $result = core_course_external::get_course_module($assign->cmid);
2610 $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
2612 $this->assertCount(0, $result['warnings']);
2613 // Test we retrieve all the fields.
2614 $this->assertCount(30, $result['cm']);
2615 $this->assertEquals($record['name'], $result['cm']['name']);
2616 $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
2617 $this->assertEquals(100, $result['cm']['grade']);
2618 $this->assertEquals(0.0, $result['cm']['gradepass']);
2619 $this->assertEquals('submissions', $result['cm']['advancedgrading'][0]['area']);
2620 $this->assertEmpty($result['cm']['advancedgrading'][0]['method']);
2621 $this->assertEquals($outcomescale, $result['cm']['outcomes'][0]['scale']);
2622 $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $result['cm']['downloadcontent']);
2624 $student = $this->getDataGenerator()->create_user();
2625 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2627 self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
2628 $this->setUser($student);
2630 // The user shouldn't be able to see the activity.
2631 try {
2632 core_course_external::get_course_module($assign->cmid);
2633 $this->fail('Exception expected due to invalid permissions.');
2634 } catch (moodle_exception $e) {
2635 $this->assertEquals('requireloginerror', $e->errorcode);
2638 // Make module visible.
2639 set_coursemodule_visible($assign->cmid, 1);
2641 // Test student user.
2642 $result = core_course_external::get_course_module($assign->cmid);
2643 $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
2645 $this->assertCount(0, $result['warnings']);
2646 // Test we retrieve only the few files we can see.
2647 $this->assertCount(12, $result['cm']);
2648 $this->assertEquals($assign->cmid, $result['cm']['id']);
2649 $this->assertEquals($course->id, $result['cm']['course']);
2650 $this->assertEquals('assign', $result['cm']['modname']);
2651 $this->assertEquals($assign->id, $result['cm']['instance']);
2656 * Test get_course_module_by_instance
2658 public function test_get_course_module_by_instance() {
2659 global $DB;
2661 $this->resetAfterTest(true);
2663 $this->setAdminUser();
2664 $course = self::getDataGenerator()->create_course();
2665 $record = array(
2666 'course' => $course->id,
2667 'name' => 'First quiz',
2668 'grade' => 90.00
2670 $options = array(
2671 'idnumber' => 'ABC',
2672 'visible' => 0
2674 // Hidden activity.
2675 $quiz = self::getDataGenerator()->create_module('quiz', $record, $options);
2677 // Test admin user can see the complete hidden activity.
2678 $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2679 $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
2681 $this->assertCount(0, $result['warnings']);
2682 // Test we retrieve all the fields.
2683 $this->assertCount(28, $result['cm']);
2684 $this->assertEquals($record['name'], $result['cm']['name']);
2685 $this->assertEquals($record['grade'], $result['cm']['grade']);
2686 $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
2687 $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $result['cm']['downloadcontent']);
2689 $student = $this->getDataGenerator()->create_user();
2690 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2692 self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
2693 $this->setUser($student);
2695 // The user shouldn't be able to see the activity.
2696 try {
2697 core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2698 $this->fail('Exception expected due to invalid permissions.');
2699 } catch (moodle_exception $e) {
2700 $this->assertEquals('requireloginerror', $e->errorcode);
2703 // Make module visible.
2704 set_coursemodule_visible($quiz->cmid, 1);
2706 // Test student user.
2707 $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2708 $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
2710 $this->assertCount(0, $result['warnings']);
2711 // Test we retrieve only the few files we can see.
2712 $this->assertCount(12, $result['cm']);
2713 $this->assertEquals($quiz->cmid, $result['cm']['id']);
2714 $this->assertEquals($course->id, $result['cm']['course']);
2715 $this->assertEquals('quiz', $result['cm']['modname']);
2716 $this->assertEquals($quiz->id, $result['cm']['instance']);
2718 // Try with an invalid module name.
2719 try {
2720 core_course_external::get_course_module_by_instance('abc', $quiz->id);
2721 $this->fail('Exception expected due to invalid module name.');
2722 } catch (dml_read_exception $e) {
2723 $this->assertEquals('dmlreadexception', $e->errorcode);
2729 * Test get_user_navigation_options
2731 public function test_get_user_navigation_options() {
2732 global $USER;
2734 $this->resetAfterTest();
2735 $course1 = self::getDataGenerator()->create_course();
2736 $course2 = self::getDataGenerator()->create_course();
2738 // Create a viewer user.
2739 $viewer = self::getDataGenerator()->create_user();
2740 $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2741 $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2743 $this->setUser($viewer->id);
2744 $courses = array($course1->id , $course2->id, SITEID);
2746 $result = core_course_external::get_user_navigation_options($courses);
2747 $result = external_api::clean_returnvalue(core_course_external::get_user_navigation_options_returns(), $result);
2749 $this->assertCount(0, $result['warnings']);
2750 $this->assertCount(3, $result['courses']);
2752 foreach ($result['courses'] as $course) {
2753 $navoptions = new stdClass;
2754 foreach ($course['options'] as $option) {
2755 $navoptions->{$option['name']} = $option['available'];
2757 $this->assertCount(9, $course['options']);
2758 if ($course['id'] == SITEID) {
2759 $this->assertTrue($navoptions->blogs);
2760 $this->assertFalse($navoptions->notes);
2761 $this->assertFalse($navoptions->participants);
2762 $this->assertTrue($navoptions->badges);
2763 $this->assertTrue($navoptions->tags);
2764 $this->assertFalse($navoptions->grades);
2765 $this->assertFalse($navoptions->search);
2766 $this->assertTrue($navoptions->competencies);
2767 $this->assertFalse($navoptions->communication);
2768 } else {
2769 $this->assertTrue($navoptions->blogs);
2770 $this->assertFalse($navoptions->notes);
2771 $this->assertTrue($navoptions->participants);
2772 $this->assertFalse($navoptions->badges);
2773 $this->assertFalse($navoptions->tags);
2774 $this->assertTrue($navoptions->grades);
2775 $this->assertFalse($navoptions->search);
2776 $this->assertTrue($navoptions->competencies);
2777 $this->assertFalse($navoptions->communication);
2783 * Test get_user_administration_options
2785 public function test_get_user_administration_options() {
2786 global $USER;
2788 $this->resetAfterTest();
2789 $course1 = self::getDataGenerator()->create_course();
2790 $course2 = self::getDataGenerator()->create_course();
2792 // Create a viewer user.
2793 $viewer = self::getDataGenerator()->create_user();
2794 $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2795 $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2797 $this->setUser($viewer->id);
2798 $courses = array($course1->id , $course2->id, SITEID);
2800 $result = core_course_external::get_user_administration_options($courses);
2801 $result = external_api::clean_returnvalue(core_course_external::get_user_administration_options_returns(), $result);
2803 $this->assertCount(0, $result['warnings']);
2804 $this->assertCount(3, $result['courses']);
2806 foreach ($result['courses'] as $course) {
2807 $adminoptions = new stdClass;
2808 foreach ($course['options'] as $option) {
2809 $adminoptions->{$option['name']} = $option['available'];
2811 if ($course['id'] == SITEID) {
2812 $this->assertCount(17, $course['options']);
2813 $this->assertFalse($adminoptions->update);
2814 $this->assertFalse($adminoptions->filters);
2815 $this->assertFalse($adminoptions->reports);
2816 $this->assertFalse($adminoptions->backup);
2817 $this->assertFalse($adminoptions->restore);
2818 $this->assertFalse($adminoptions->files);
2819 $this->assertFalse(!isset($adminoptions->tags));
2820 $this->assertFalse($adminoptions->gradebook);
2821 $this->assertFalse($adminoptions->outcomes);
2822 $this->assertFalse($adminoptions->badges);
2823 $this->assertFalse($adminoptions->import);
2824 $this->assertFalse($adminoptions->reset);
2825 $this->assertFalse($adminoptions->roles);
2826 $this->assertFalse($adminoptions->editcompletion);
2827 $this->assertFalse($adminoptions->copy);
2828 } else {
2829 $this->assertCount(15, $course['options']);
2830 $this->assertFalse($adminoptions->update);
2831 $this->assertFalse($adminoptions->filters);
2832 $this->assertFalse($adminoptions->reports);
2833 $this->assertFalse($adminoptions->backup);
2834 $this->assertFalse($adminoptions->restore);
2835 $this->assertFalse($adminoptions->files);
2836 $this->assertFalse($adminoptions->tags);
2837 $this->assertFalse($adminoptions->gradebook);
2838 $this->assertFalse($adminoptions->outcomes);
2839 $this->assertTrue($adminoptions->badges);
2840 $this->assertFalse($adminoptions->import);
2841 $this->assertFalse($adminoptions->reset);
2842 $this->assertFalse($adminoptions->roles);
2843 $this->assertFalse($adminoptions->editcompletion);
2844 $this->assertFalse($adminoptions->copy);
2850 * Test get_courses_by_fields
2852 public function test_get_courses_by_field() {
2853 global $DB, $USER;
2854 $this->resetAfterTest(true);
2856 $this->setAdminUser();
2858 $category1 = self::getDataGenerator()->create_category(array('name' => 'Cat 1'));
2859 $category2 = self::getDataGenerator()->create_category(array('parent' => $category1->id));
2860 $course1 = self::getDataGenerator()->create_course(
2861 array('category' => $category1->id, 'shortname' => 'c1', 'format' => 'topics'));
2863 $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
2864 $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
2865 'categoryid' => $fieldcategory->get('id')];
2866 $field = self::getDataGenerator()->create_custom_field($customfield);
2867 $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
2868 // Create course image.
2869 $draftid = file_get_unused_draft_itemid();
2870 $filerecord = [
2871 'component' => 'user',
2872 'filearea' => 'draft',
2873 'contextid' => context_user::instance($USER->id)->id,
2874 'itemid' => $draftid,
2875 'filename' => 'image.jpg',
2876 'filepath' => '/',
2878 $fs = get_file_storage();
2879 $fs->create_file_from_pathname($filerecord, __DIR__ . '/fixtures/image.jpg');
2880 $course2 = self::getDataGenerator()->create_course([
2881 'visible' => 0,
2882 'category' => $category2->id,
2883 'idnumber' => 'i2',
2884 'customfields' => [$customfieldvalue],
2885 'overviewfiles_filemanager' => $draftid
2888 $student1 = self::getDataGenerator()->create_user();
2889 $user1 = self::getDataGenerator()->create_user();
2890 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2891 self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id);
2892 self::getDataGenerator()->enrol_user($student1->id, $course2->id, $studentrole->id);
2894 self::setAdminUser();
2895 // As admins, we should be able to retrieve everything.
2896 $result = core_course_external::get_courses_by_field();
2897 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2898 $this->assertCount(3, $result['courses']);
2899 // Expect to receive all the fields.
2900 $this->assertCount(41, $result['courses'][0]);
2901 $this->assertCount(42, $result['courses'][1]); // One more field because is not the site course.
2902 $this->assertCount(42, $result['courses'][2]); // One more field because is not the site course.
2904 $result = core_course_external::get_courses_by_field('id', $course1->id);
2905 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2906 $this->assertCount(1, $result['courses']);
2907 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2908 // Expect to receive all the fields.
2909 $this->assertCount(42, $result['courses'][0]);
2910 // Check default values for course format topics.
2911 $this->assertCount(3, $result['courses'][0]['courseformatoptions']);
2912 foreach ($result['courses'][0]['courseformatoptions'] as $option) {
2913 switch ($option['name']) {
2914 case 'hiddensections':
2915 $this->assertEquals(1, $option['value']);
2916 break;
2917 case 'coursedisplay':
2918 $this->assertEquals(0, $option['value']);
2919 break;
2920 case 'indentation':
2921 $this->assertEquals(1, $option['value']);
2922 break;
2923 default:
2926 $this->assertStringContainsString('/course/generated', $result['courses'][0]['courseimage']);
2928 $result = core_course_external::get_courses_by_field('id', $course2->id);
2929 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2930 $this->assertCount(1, $result['courses']);
2931 $this->assertEquals($course2->id, $result['courses'][0]['id']);
2932 // Check custom fields properly returned.
2933 $this->assertEquals([
2934 'shortname' => $customfield['shortname'],
2935 'name' => $customfield['name'],
2936 'type' => $customfield['type'],
2937 'value' => $customfieldvalue['value'],
2938 'valueraw' => $customfieldvalue['value'],
2939 ], $result['courses'][0]['customfields'][0]);
2940 $this->assertStringContainsString('/course/overviewfiles', $result['courses'][0]['courseimage']);
2942 $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2943 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2944 $this->assertCount(2, $result['courses']);
2946 // Check default filters.
2947 $this->assertCount(6, $result['courses'][0]['filters']);
2948 $this->assertCount(6, $result['courses'][1]['filters']);
2950 $result = core_course_external::get_courses_by_field('category', $category1->id);
2951 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2952 $this->assertCount(1, $result['courses']);
2953 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2954 $this->assertEquals('Cat 1', $result['courses'][0]['categoryname']);
2956 $result = core_course_external::get_courses_by_field('shortname', 'c1');
2957 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2958 $this->assertCount(1, $result['courses']);
2959 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2961 $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2962 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2963 $this->assertCount(1, $result['courses']);
2964 $this->assertEquals($course2->id, $result['courses'][0]['id']);
2966 $result = core_course_external::get_courses_by_field('idnumber', 'x');
2967 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2968 $this->assertCount(0, $result['courses']);
2970 // Change filter value.
2971 filter_set_local_state('mediaplugin', context_course::instance($course1->id)->id, TEXTFILTER_OFF);
2973 self::setUser($student1);
2974 // All visible courses (including front page) for normal student.
2975 $result = core_course_external::get_courses_by_field();
2976 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2977 $this->assertCount(2, $result['courses']);
2978 $this->assertCount(34, $result['courses'][0]);
2979 $this->assertCount(35, $result['courses'][1]); // One field more (course format options), not present in site course.
2981 $result = core_course_external::get_courses_by_field('id', $course1->id);
2982 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2983 $this->assertCount(1, $result['courses']);
2984 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2985 // Expect to receive all the files that a student can see.
2986 $this->assertCount(35, $result['courses'][0]);
2988 // Check default filters.
2989 $filters = $result['courses'][0]['filters'];
2990 $this->assertCount(6, $filters);
2991 $found = false;
2992 foreach ($filters as $filter) {
2993 if ($filter['filter'] == 'mediaplugin' and $filter['localstate'] == TEXTFILTER_OFF) {
2994 $found = true;
2997 $this->assertTrue($found);
2999 // Course 2 is not visible.
3000 $result = core_course_external::get_courses_by_field('id', $course2->id);
3001 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3002 $this->assertCount(0, $result['courses']);
3004 $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
3005 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3006 $this->assertCount(1, $result['courses']);
3008 $result = core_course_external::get_courses_by_field('category', $category1->id);
3009 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3010 $this->assertCount(1, $result['courses']);
3011 $this->assertEquals($course1->id, $result['courses'][0]['id']);
3013 $result = core_course_external::get_courses_by_field('shortname', 'c1');
3014 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3015 $this->assertCount(1, $result['courses']);
3016 $this->assertEquals($course1->id, $result['courses'][0]['id']);
3018 $result = core_course_external::get_courses_by_field('idnumber', 'i2');
3019 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3020 $this->assertCount(0, $result['courses']);
3022 $result = core_course_external::get_courses_by_field('idnumber', 'x');
3023 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3024 $this->assertCount(0, $result['courses']);
3026 self::setUser($user1);
3027 // All visible courses (including front page) for authenticated user.
3028 $result = core_course_external::get_courses_by_field();
3029 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3030 $this->assertCount(2, $result['courses']);
3031 $this->assertCount(34, $result['courses'][0]); // Site course.
3032 $this->assertCount(17, $result['courses'][1]); // Only public information, not enrolled.
3034 $result = core_course_external::get_courses_by_field('id', $course1->id);
3035 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3036 $this->assertCount(1, $result['courses']);
3037 $this->assertEquals($course1->id, $result['courses'][0]['id']);
3038 // Expect to receive all the files that a authenticated can see.
3039 $this->assertCount(17, $result['courses'][0]);
3041 // Course 2 is not visible.
3042 $result = core_course_external::get_courses_by_field('id', $course2->id);
3043 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3044 $this->assertCount(0, $result['courses']);
3046 $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
3047 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3048 $this->assertCount(1, $result['courses']);
3050 $result = core_course_external::get_courses_by_field('category', $category1->id);
3051 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3052 $this->assertCount(1, $result['courses']);
3053 $this->assertEquals($course1->id, $result['courses'][0]['id']);
3055 $result = core_course_external::get_courses_by_field('shortname', 'c1');
3056 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3057 $this->assertCount(1, $result['courses']);
3058 $this->assertEquals($course1->id, $result['courses'][0]['id']);
3060 $result = core_course_external::get_courses_by_field('idnumber', 'i2');
3061 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3062 $this->assertCount(0, $result['courses']);
3064 $result = core_course_external::get_courses_by_field('idnumber', 'x');
3065 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3066 $this->assertCount(0, $result['courses']);
3070 * Test retrieving courses by field returns custom field data
3072 public function test_get_courses_by_field_customfields(): void {
3073 $this->resetAfterTest();
3074 $this->setAdminUser();
3076 $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
3077 $datefield = $this->getDataGenerator()->create_custom_field([
3078 'categoryid' => $fieldcategory->get('id'),
3079 'shortname' => 'mydate',
3080 'name' => 'My date',
3081 'type' => 'date',
3084 $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
3086 'shortname' => $datefield->get('shortname'),
3087 'value' => 1580389200, // 30/01/2020 13:00 GMT.
3089 ]]);
3091 $result = external_api::clean_returnvalue(
3092 core_course_external::get_courses_by_field_returns(),
3093 core_course_external::get_courses_by_field('id', $newcourse->id)
3096 $this->assertCount(1, $result['courses']);
3097 $course = reset($result['courses']);
3099 $this->assertArrayHasKey('customfields', $course);
3100 $this->assertCount(1, $course['customfields']);
3102 // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
3103 $this->assertEquals([
3104 'name' => $datefield->get('name'),
3105 'shortname' => $datefield->get('shortname'),
3106 'type' => $datefield->get('type'),
3107 'value' => userdate(1580389200),
3108 'valueraw' => 1580389200,
3109 ], reset($course['customfields']));
3113 * Test retrieving courses by field returning communication tools.
3114 * @covers \core_course_external::get_courses_by_field
3116 public function test_get_courses_by_field_communication(): void {
3117 $this->resetAfterTest();
3118 $this->setAdminUser();
3120 // Create communication tool in course.
3121 set_config('enablecommunicationsubsystem', 1);
3123 $roomname = 'Course chat';
3124 $telegramlink = 'https://my.telegram.chat/120';
3125 $record = [
3126 'selectedcommunication' => 'communication_customlink',
3127 'communicationroomname' => $roomname,
3128 'customlinkurl' => $telegramlink,
3130 $course = $this->getDataGenerator()->create_course($record);
3131 $communication = \core_communication\api::load_by_instance(
3132 context: \core\context\course::instance($course->id),
3133 component: 'core_course',
3134 instancetype: 'coursecommunication',
3135 instanceid: $course->id,
3138 $result = external_api::clean_returnvalue(
3139 core_course_external::get_courses_by_field_returns(),
3140 core_course_external::get_courses_by_field('id', $course->id)
3143 $course = reset($result['courses']);
3144 $this->assertEquals($roomname, $course['communicationroomname']);
3145 $this->assertEquals($telegramlink, $course['communicationroomurl']);
3147 // Course without comm tools.
3148 $course = $this->getDataGenerator()->create_course();
3149 $result = external_api::clean_returnvalue(
3150 core_course_external::get_courses_by_field_returns(),
3151 core_course_external::get_courses_by_field('id', $course->id)
3154 $course = reset($result['courses']);
3155 $this->assertNotContains('communicationroomname', $course);
3156 $this->assertNotContains('communicationroomurl', $course);
3159 public function test_get_courses_by_field_invalid_field() {
3160 $this->expectException('invalid_parameter_exception');
3161 $result = core_course_external::get_courses_by_field('zyx', 'x');
3164 public function test_get_courses_by_field_invalid_courses() {
3165 $result = core_course_external::get_courses_by_field('id', '-1');
3166 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3167 $this->assertCount(0, $result['courses']);
3171 * Test get_courses_by_field_invalid_theme_and_lang
3173 public function test_get_courses_by_field_invalid_theme_and_lang() {
3174 $this->resetAfterTest(true);
3175 $this->setAdminUser();
3177 $course = self::getDataGenerator()->create_course(array('theme' => 'kkt', 'lang' => 'kkl'));
3178 $result = core_course_external::get_courses_by_field('id', $course->id);
3179 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3180 $this->assertEmpty($result['courses']['0']['theme']);
3181 $this->assertEmpty($result['courses']['0']['lang']);
3185 public function test_check_updates() {
3186 global $DB;
3187 $this->resetAfterTest(true);
3188 $this->setAdminUser();
3190 // Create different types of activities.
3191 $course = self::getDataGenerator()->create_course();
3192 $tocreate = [
3193 'assign',
3194 'book',
3195 'choice',
3196 'folder',
3197 'forum',
3198 'glossary',
3199 'imscp',
3200 'label',
3201 'lesson',
3202 'lti',
3203 'page',
3204 'quiz',
3205 'resource',
3206 'scorm',
3207 'url',
3208 'wiki',
3211 $modules = array();
3212 foreach ($tocreate as $modname) {
3213 $modules[$modname]['instance'] = $this->getDataGenerator()->create_module($modname, array('course' => $course->id));
3214 $modules[$modname]['cm'] = get_coursemodule_from_id(false, $modules[$modname]['instance']->cmid);
3215 $modules[$modname]['context'] = context_module::instance($modules[$modname]['instance']->cmid);
3218 $student = self::getDataGenerator()->create_user();
3219 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
3220 self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
3221 $this->setUser($student);
3223 $since = time();
3224 $this->waitForSecond();
3225 $params = array();
3226 foreach ($modules as $modname => $data) {
3227 $params[$data['cm']->id] = array(
3228 'contextlevel' => 'module',
3229 'id' => $data['cm']->id,
3230 'since' => $since
3234 // Check there is nothing updated because modules are fresh new.
3235 $result = core_course_external::check_updates($course->id, $params);
3236 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3237 $this->assertCount(0, $result['instances']);
3238 $this->assertCount(0, $result['warnings']);
3240 // Test with get_updates_since the same data.
3241 $result = core_course_external::get_updates_since($course->id, $since);
3242 $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
3243 $this->assertCount(0, $result['instances']);
3244 $this->assertCount(0, $result['warnings']);
3246 // Update a module after a second.
3247 $this->waitForSecond();
3248 set_coursemodule_name($modules['forum']['cm']->id, 'New forum name');
3250 $found = false;
3251 $result = core_course_external::check_updates($course->id, $params);
3252 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3253 $this->assertCount(1, $result['instances']);
3254 $this->assertCount(0, $result['warnings']);
3255 foreach ($result['instances'] as $module) {
3256 foreach ($module['updates'] as $update) {
3257 if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
3258 $found = true;
3262 $this->assertTrue($found);
3264 // Test with get_updates_since the same data.
3265 $result = core_course_external::get_updates_since($course->id, $since);
3266 $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
3267 $this->assertCount(1, $result['instances']);
3268 $this->assertCount(0, $result['warnings']);
3269 $found = false;
3270 $this->assertCount(1, $result['instances']);
3271 $this->assertCount(0, $result['warnings']);
3272 foreach ($result['instances'] as $module) {
3273 foreach ($module['updates'] as $update) {
3274 if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
3275 $found = true;
3279 $this->assertTrue($found);
3281 // Do not retrieve the configuration field.
3282 $filter = array('files');
3283 $found = false;
3284 $result = core_course_external::check_updates($course->id, $params, $filter);
3285 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3286 $this->assertCount(0, $result['instances']);
3287 $this->assertCount(0, $result['warnings']);
3288 $this->assertFalse($found);
3290 // Add invalid cmid.
3291 $params[] = array(
3292 'contextlevel' => 'module',
3293 'id' => -2,
3294 'since' => $since
3296 $result = core_course_external::check_updates($course->id, $params);
3297 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3298 $this->assertCount(1, $result['warnings']);
3299 $this->assertEquals(-2, $result['warnings'][0]['itemid']);
3303 * Test cases for the get_enrolled_courses_by_timeline_classification test.
3305 public function get_get_enrolled_courses_by_timeline_classification_test_cases(): array {
3306 $now = time();
3307 $day = 86400;
3309 $coursedata = [
3311 'shortname' => 'apast',
3312 'startdate' => $now - ($day * 2),
3313 'enddate' => $now - $day
3316 'shortname' => 'bpast',
3317 'startdate' => $now - ($day * 2),
3318 'enddate' => $now - $day
3321 'shortname' => 'cpast',
3322 'startdate' => $now - ($day * 2),
3323 'enddate' => $now - $day
3326 'shortname' => 'dpast',
3327 'startdate' => $now - ($day * 2),
3328 'enddate' => $now - $day
3331 'shortname' => 'epast',
3332 'startdate' => $now - ($day * 2),
3333 'enddate' => $now - $day
3336 'shortname' => 'ainprogress',
3337 'startdate' => $now - $day,
3338 'enddate' => $now + $day
3341 'shortname' => 'binprogress',
3342 'startdate' => $now - $day,
3343 'enddate' => $now + $day
3346 'shortname' => 'cinprogress',
3347 'startdate' => $now - $day,
3348 'enddate' => $now + $day
3351 'shortname' => 'dinprogress',
3352 'startdate' => $now - $day,
3353 'enddate' => $now + $day
3356 'shortname' => 'einprogress',
3357 'startdate' => $now - $day,
3358 'enddate' => $now + $day
3361 'shortname' => 'afuture',
3362 'startdate' => $now + $day
3365 'shortname' => 'bfuture',
3366 'startdate' => $now + $day
3369 'shortname' => 'cfuture',
3370 'startdate' => $now + $day
3373 'shortname' => 'dfuture',
3374 'startdate' => $now + $day
3377 'shortname' => 'efuture',
3378 'startdate' => $now + $day
3382 // Raw enrolled courses result set should be returned in this order:
3383 // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
3384 // dfuture, dinprogress, dpast, efuture, einprogress, epast
3386 // By classification the offset values for each record should be:
3387 // COURSE_TIMELINE_FUTURE
3388 // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
3389 // COURSE_TIMELINE_INPROGRESS
3390 // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
3391 // COURSE_TIMELINE_PAST
3392 // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
3394 // NOTE: The offset applies to the unfiltered full set of courses before the classification
3395 // filtering is done.
3396 // E.g. In our example if an offset of 2 is given then it would mean the first
3397 // two courses (afuture, ainprogress) are ignored.
3398 return [
3399 'empty set' => [
3400 'coursedata' => [],
3401 'classification' => 'future',
3402 'limit' => 2,
3403 'offset' => 0,
3404 'sort' => 'shortname ASC',
3405 'expectedcourses' => [],
3406 'expectednextoffset' => 0,
3408 // COURSE_TIMELINE_FUTURE.
3409 'future not limit no offset' => [
3410 'coursedata' => $coursedata,
3411 'classification' => 'future',
3412 'limit' => 0,
3413 'offset' => 0,
3414 'sort' => 'shortname ASC',
3415 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3416 'expectednextoffset' => 15,
3418 'future no offset' => [
3419 'coursedata' => $coursedata,
3420 'classification' => 'future',
3421 'limit' => 2,
3422 'offset' => 0,
3423 'sort' => 'shortname ASC',
3424 'expectedcourses' => ['afuture', 'bfuture'],
3425 'expectednextoffset' => 4,
3427 'future offset' => [
3428 'coursedata' => $coursedata,
3429 'classification' => 'future',
3430 'limit' => 2,
3431 'offset' => 2,
3432 'sort' => 'shortname ASC',
3433 'expectedcourses' => ['bfuture', 'cfuture'],
3434 'expectednextoffset' => 7,
3436 'future exact limit' => [
3437 'coursedata' => $coursedata,
3438 'classification' => 'future',
3439 'limit' => 5,
3440 'offset' => 0,
3441 'sort' => 'shortname ASC',
3442 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3443 'expectednextoffset' => 13,
3445 'future limit less results' => [
3446 'coursedata' => $coursedata,
3447 'classification' => 'future',
3448 'limit' => 10,
3449 'offset' => 0,
3450 'sort' => 'shortname ASC',
3451 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3452 'expectednextoffset' => 15,
3454 'future limit less results with offset' => [
3455 'coursedata' => $coursedata,
3456 'classification' => 'future',
3457 'limit' => 10,
3458 'offset' => 5,
3459 'sort' => 'shortname ASC',
3460 'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
3461 'expectednextoffset' => 15,
3463 'all no limit or offset' => [
3464 'coursedata' => $coursedata,
3465 'classification' => 'all',
3466 'limit' => 0,
3467 'offset' => 0,
3468 'sort' => 'shortname ASC',
3469 'expectedcourses' => [
3470 'afuture',
3471 'ainprogress',
3472 'apast',
3473 'bfuture',
3474 'binprogress',
3475 'bpast',
3476 'cfuture',
3477 'cinprogress',
3478 'cpast',
3479 'dfuture',
3480 'dinprogress',
3481 'dpast',
3482 'efuture',
3483 'einprogress',
3484 'epast'
3486 'expectednextoffset' => 15,
3488 'all limit no offset' => [
3489 'coursedata' => $coursedata,
3490 'classification' => 'all',
3491 'limit' => 5,
3492 'offset' => 0,
3493 'sort' => 'shortname ASC',
3494 'expectedcourses' => [
3495 'afuture',
3496 'ainprogress',
3497 'apast',
3498 'bfuture',
3499 'binprogress'
3501 'expectednextoffset' => 5,
3503 'all limit and offset' => [
3504 'coursedata' => $coursedata,
3505 'classification' => 'all',
3506 'limit' => 5,
3507 'offset' => 5,
3508 'sort' => 'shortname ASC',
3509 'expectedcourses' => [
3510 'bpast',
3511 'cfuture',
3512 'cinprogress',
3513 'cpast',
3514 'dfuture'
3516 'expectednextoffset' => 10,
3518 'all offset past result set' => [
3519 'coursedata' => $coursedata,
3520 'classification' => 'all',
3521 'limit' => 5,
3522 'offset' => 50,
3523 'sort' => 'shortname ASC',
3524 'expectedcourses' => [],
3525 'expectednextoffset' => 50,
3527 'all limit and offset with sort ul.timeaccess desc' => [
3528 'coursedata' => $coursedata,
3529 'classification' => 'inprogress',
3530 'limit' => 0,
3531 'offset' => 0,
3532 'sort' => 'ul.timeaccess desc',
3533 'expectedcourses' => [
3534 'ainprogress',
3535 'binprogress',
3536 'cinprogress',
3537 'dinprogress',
3538 'einprogress'
3540 'expectednextoffset' => 15,
3542 'all limit and offset with sort sql injection for sort or 1==1' => [
3543 'coursedata' => $coursedata,
3544 'classification' => 'all',
3545 'limit' => 5,
3546 'offset' => 5,
3547 'sort' => 'ul.timeaccess desc or 1==1',
3548 'expectedcourses' => [],
3549 'expectednextoffset' => 0,
3550 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3552 'all limit and offset with sql injection of sort a custom one' => [
3553 'coursedata' => $coursedata,
3554 'classification' => 'all',
3555 'limit' => 5,
3556 'offset' => 5,
3557 'sort' => "ul.timeaccess LIMIT 1--",
3558 'expectedcourses' => [],
3559 'expectednextoffset' => 0,
3560 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3562 'all limit and offset with wrong sort direction' => [
3563 'coursedata' => $coursedata,
3564 'classification' => 'all',
3565 'limit' => 5,
3566 'offset' => 5,
3567 'sort' => "ul.timeaccess abcdasc",
3568 'expectedcourses' => [],
3569 'expectednextoffset' => 0,
3570 'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()',
3572 'all limit and offset with wrong sort direction' => [
3573 'coursedata' => $coursedata,
3574 'classification' => 'all',
3575 'limit' => 5,
3576 'offset' => 5,
3577 'sort' => "ul.timeaccess.foo ascd",
3578 'expectedcourses' => [],
3579 'expectednextoffset' => 0,
3580 'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()',
3582 'all limit and offset with wrong sort param' => [
3583 'coursedata' => $coursedata,
3584 'classification' => 'all',
3585 'limit' => 5,
3586 'offset' => 5,
3587 'sort' => "foobar",
3588 'expectedcourses' => [],
3589 'expectednextoffset' => 0,
3590 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3592 'all limit and offset with wrong field name' => [
3593 'coursedata' => $coursedata,
3594 'classification' => 'all',
3595 'limit' => 5,
3596 'offset' => 5,
3597 'sort' => "ul.foobar",
3598 'expectedcourses' => [],
3599 'expectednextoffset' => 0,
3600 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3602 'all limit and offset with wrong field separator' => [
3603 'coursedata' => $coursedata,
3604 'classification' => 'all',
3605 'limit' => 5,
3606 'offset' => 5,
3607 'sort' => "ul.timeaccess.foo",
3608 'expectedcourses' => [],
3609 'expectednextoffset' => 0,
3610 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3612 'all limit and offset with wrong field separator #' => [
3613 'coursedata' => $coursedata,
3614 'classification' => 'all',
3615 'limit' => 5,
3616 'offset' => 5,
3617 'sort' => "ul#timeaccess",
3618 'expectedcourses' => [],
3619 'expectednextoffset' => 0,
3620 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3622 'all limit and offset with wrong field separator $' => [
3623 'coursedata' => $coursedata,
3624 'classification' => 'all',
3625 'limit' => 5,
3626 'offset' => 5,
3627 'sort' => 'ul$timeaccess',
3628 'expectedcourses' => [],
3629 'expectednextoffset' => 0,
3630 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3632 'all limit and offset with wrong field name' => [
3633 'coursedata' => $coursedata,
3634 'classification' => 'all',
3635 'limit' => 5,
3636 'offset' => 5,
3637 'sort' => 'timeaccess123',
3638 'expectedcourses' => [],
3639 'expectednextoffset' => 0,
3640 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3642 'all limit and offset with no sort direction for ul' => [
3643 'coursedata' => $coursedata,
3644 'classification' => 'inprogress',
3645 'limit' => 0,
3646 'offset' => 0,
3647 'sort' => "ul.timeaccess",
3648 'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3649 'expectednextoffset' => 15,
3651 'all limit and offset with valid field name and no prefix, test for ul' => [
3652 'coursedata' => $coursedata,
3653 'classification' => 'inprogress',
3654 'limit' => 0,
3655 'offset' => 0,
3656 'sort' => "timeaccess",
3657 'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3658 'expectednextoffset' => 15,
3660 'all limit and offset with valid field name and no prefix' => [
3661 'coursedata' => $coursedata,
3662 'classification' => 'all',
3663 'limit' => 5,
3664 'offset' => 5,
3665 'sort' => "fullname",
3666 'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
3667 'expectednextoffset' => 10,
3669 'all limit and offset with valid field name and no prefix and with sort direction' => [
3670 'coursedata' => $coursedata,
3671 'classification' => 'all',
3672 'limit' => 5,
3673 'offset' => 5,
3674 'sort' => "fullname desc",
3675 'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
3676 'expectednextoffset' => 10,
3678 'Search courses for courses containing bfut' => [
3679 'coursedata' => $coursedata,
3680 'classification' => 'search',
3681 'limit' => 0,
3682 'offset' => 0,
3683 'sort' => null,
3684 'expectedcourses' => ['bfuture'],
3685 'expectednextoffset' => 1,
3686 'expectedexception' => null,
3687 'searchvalue' => 'bfut',
3689 'Search courses for courses containing inp' => [
3690 'coursedata' => $coursedata,
3691 'classification' => 'search',
3692 'limit' => 0,
3693 'offset' => 0,
3694 'sort' => null,
3695 'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3696 'expectednextoffset' => 5,
3697 'expectedexception' => null,
3698 'searchvalue' => 'inp',
3700 'Search courses for courses containing fail' => [
3701 'coursedata' => $coursedata,
3702 'classification' => 'search',
3703 'limit' => 0,
3704 'offset' => 0,
3705 'sort' => null,
3706 'expectedcourses' => [],
3707 'expectednextoffset' => 0,
3708 'expectedexception' => null,
3709 'searchvalue' => 'fail',
3711 'Search courses for courses containing !`~[]C' => [
3712 'coursedata' => $coursedata,
3713 'classification' => 'search',
3714 'limit' => 0,
3715 'offset' => 0,
3716 'sort' => null,
3717 'expectedcourses' => [],
3718 'expectednextoffset' => 0,
3719 'expectedexception' => null,
3720 'searchvalue' => '!`~[]C',
3726 * Test the get_enrolled_courses_by_timeline_classification function.
3728 * @dataProvider get_get_enrolled_courses_by_timeline_classification_test_cases()
3729 * @param array $coursedata Courses to create
3730 * @param string $classification Timeline classification
3731 * @param int $limit Maximum number of results
3732 * @param int $offset Offset the unfiltered courses result set by this amount
3733 * @param string $sort sort the courses
3734 * @param array $expectedcourses Expected courses in result
3735 * @param int $expectednextoffset Expected next offset value in result
3736 * @param string|null $expectedexception Expected exception string
3737 * @param string|null $searchvalue If we are searching, what do we need to look for?
3739 public function test_get_enrolled_courses_by_timeline_classification(
3740 $coursedata,
3741 $classification,
3742 $limit,
3743 $offset,
3744 $sort,
3745 $expectedcourses,
3746 $expectednextoffset,
3747 $expectedexception = null,
3748 $searchvalue = null
3750 $this->resetAfterTest();
3751 $generator = $this->getDataGenerator();
3753 $courses = array_map(function($coursedata) use ($generator) {
3754 return $generator->create_course($coursedata);
3755 }, $coursedata);
3757 $student = $generator->create_user();
3759 foreach ($courses as $course) {
3760 $generator->enrol_user($student->id, $course->id, 'student');
3763 $this->setUser($student);
3765 if (isset($expectedexception)) {
3766 $this->expectException('coding_exception');
3767 $this->expectExceptionMessage($expectedexception);
3770 // NOTE: The offset applies to the unfiltered full set of courses before the classification
3771 // filtering is done.
3772 // E.g. In our example if an offset of 2 is given then it would mean the first
3773 // two courses (afuture, ainprogress) are ignored.
3774 $result = core_course_external::get_enrolled_courses_by_timeline_classification(
3775 $classification,
3776 $limit,
3777 $offset,
3778 $sort,
3779 null,
3780 null,
3781 $searchvalue
3783 $result = external_api::clean_returnvalue(
3784 core_course_external::get_enrolled_courses_by_timeline_classification_returns(),
3785 $result
3788 $actual = array_map(function($course) {
3789 return $course['shortname'];
3790 }, $result['courses']);
3792 $this->assertEqualsCanonicalizing($expectedcourses, $actual);
3793 $this->assertEquals($expectednextoffset, $result['nextoffset']);
3797 * Test the get_recent_courses function.
3799 public function test_get_recent_courses() {
3800 global $USER, $DB;
3802 $this->resetAfterTest();
3803 $generator = $this->getDataGenerator();
3805 set_config('hiddenuserfields', 'lastaccess');
3807 $courses = array();
3808 for ($i = 1; $i < 12; $i++) {
3809 $courses[] = $generator->create_course();
3812 $student = $generator->create_user();
3813 $teacher = $generator->create_user();
3815 foreach ($courses as $course) {
3816 $generator->enrol_user($student->id, $course->id, 'student');
3819 $generator->enrol_user($teacher->id, $courses[0]->id, 'teacher');
3821 $this->setUser($student);
3823 $result = core_course_external::get_recent_courses($USER->id);
3825 // No course accessed.
3826 $this->assertCount(0, $result);
3828 foreach ($courses as $course) {
3829 core_course_external::view_course($course->id);
3832 // Every course accessed.
3833 $result = core_course_external::get_recent_courses($USER->id);
3834 $this->assertCount( 11, $result);
3836 // Every course accessed, result limited to 10 courses.
3837 $result = core_course_external::get_recent_courses($USER->id, 10);
3838 $this->assertCount(10, $result);
3840 $guestcourse = $generator->create_course(
3841 (object)array('shortname' => 'guestcourse',
3842 'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
3843 'enrol_guest_password_0' => ''));
3844 core_course_external::view_course($guestcourse->id);
3846 // Every course accessed, even the not enrolled one.
3847 $result = core_course_external::get_recent_courses($USER->id);
3848 $this->assertCount(12, $result);
3850 // Offset 5, return 7 out of 12.
3851 $result = core_course_external::get_recent_courses($USER->id, 0, 5);
3852 $this->assertCount(7, $result);
3854 // Offset 5 and limit 3, return 3 out of 12.
3855 $result = core_course_external::get_recent_courses($USER->id, 3, 5);
3856 $this->assertCount(3, $result);
3858 // Sorted by course id ASC.
3859 $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id ASC');
3860 $this->assertEquals($courses[0]->id, array_shift($result)->id);
3862 // Sorted by course id DESC.
3863 $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id DESC');
3864 $this->assertEquals($guestcourse->id, array_shift($result)->id);
3866 // If last access is hidden, only get the courses where has viewhiddenuserfields capability.
3867 $this->setUser($teacher);
3868 $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
3869 $usercontext = context_user::instance($student->id);
3870 $this->assignUserCapability('moodle/user:viewdetails', $usercontext, $teacherroleid);
3872 // Sorted by course id DESC.
3873 $result = core_course_external::get_recent_courses($student->id);
3874 $this->assertCount(1, $result);
3875 $this->assertEquals($courses[0]->id, array_shift($result)->id);
3879 * Test get enrolled users by cmid function.
3881 public function test_get_enrolled_users_by_cmid() {
3882 global $PAGE;
3883 $this->resetAfterTest(true);
3885 $user1 = self::getDataGenerator()->create_user();
3886 $user2 = self::getDataGenerator()->create_user();
3887 $user3 = self::getDataGenerator()->create_user();
3889 $user1picture = new user_picture($user1);
3890 $user1picture->size = 1;
3891 $user1->profileimage = $user1picture->get_url($PAGE)->out(false);
3893 $user2picture = new user_picture($user2);
3894 $user2picture->size = 1;
3895 $user2->profileimage = $user2picture->get_url($PAGE)->out(false);
3897 $user3picture = new user_picture($user3);
3898 $user3picture->size = 1;
3899 $user3->profileimage = $user3picture->get_url($PAGE)->out(false);
3901 // Set the first created user to the test user.
3902 self::setUser($user1);
3904 // Create course to add the module.
3905 $course1 = self::getDataGenerator()->create_course();
3907 // Forum with tracking off.
3908 $record = new stdClass();
3909 $record->course = $course1->id;
3910 $forum1 = self::getDataGenerator()->create_module('forum', $record);
3912 // Following lines enrol and assign default role id to the users.
3913 $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
3914 $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
3915 // Enrol a suspended user in the course.
3916 $this->getDataGenerator()->enrol_user($user3->id, $course1->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
3918 // Create what we expect to be returned when querying the course module.
3919 $expectedusers = array(
3920 'users' => array(),
3921 'warnings' => array(),
3924 $expectedusers['users'][0] = [
3925 'id' => $user1->id,
3926 'fullname' => fullname($user1),
3927 'firstname' => $user1->firstname,
3928 'lastname' => $user1->lastname,
3929 'profileimage' => $user1->profileimage,
3931 $expectedusers['users'][1] = [
3932 'id' => $user2->id,
3933 'fullname' => fullname($user2),
3934 'firstname' => $user2->firstname,
3935 'lastname' => $user2->lastname,
3936 'profileimage' => $user2->profileimage,
3938 $expectedusers['users'][2] = [
3939 'id' => $user3->id,
3940 'fullname' => fullname($user3),
3941 'firstname' => $user3->firstname,
3942 'lastname' => $user3->lastname,
3943 'profileimage' => $user3->profileimage,
3946 // Test getting the users in a given context.
3947 $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid);
3948 $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users);
3950 $this->assertEquals(3, count($users['users']));
3951 $this->assertEquals($expectedusers, $users);
3953 // Test getting only the active users in a given context.
3954 $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid, 0, true);
3955 $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users);
3957 $expectedusers['users'] = [
3959 'id' => $user1->id,
3960 'fullname' => fullname($user1),
3961 'firstname' => $user1->firstname,
3962 'lastname' => $user1->lastname,
3963 'profileimage' => $user1->profileimage,
3966 'id' => $user2->id,
3967 'fullname' => fullname($user2),
3968 'firstname' => $user2->firstname,
3969 'lastname' => $user2->lastname,
3970 'profileimage' => $user2->profileimage,
3974 $this->assertEquals(2, count($users['users']));
3975 $this->assertEquals($expectedusers, $users);
3979 * Verify that content items can be added to user favourites.
3981 public function test_add_content_item_to_user_favourites() {
3982 $this->resetAfterTest();
3984 $course = $this->getDataGenerator()->create_course();
3985 $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
3986 $this->setUser($user);
3988 // Using the internal API, confirm that no items are set as favourites for the user.
3989 $contentitemservice = new \core_course\local\service\content_item_service(
3990 new \core_course\local\repository\content_item_readonly_repository()
3992 $contentitems = $contentitemservice->get_all_content_items($user);
3993 $favourited = array_filter($contentitems, function($contentitem) {
3994 return $contentitem->favourite == true;
3996 $this->assertCount(0, $favourited);
3998 // Using the external API, favourite a content item for the user.
3999 $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))];
4000 $contentitem = core_course_external::add_content_item_to_user_favourites('mod_assign', $assign->id, $user->id);
4001 $contentitem = external_api::clean_returnvalue(core_course_external::add_content_item_to_user_favourites_returns(),
4002 $contentitem);
4004 // Verify the returned item is a favourite.
4005 $this->assertTrue($contentitem['favourite']);
4007 // Using the internal API, confirm we see a single favourite item.
4008 $contentitems = $contentitemservice->get_all_content_items($user);
4009 $favourited = array_values(array_filter($contentitems, function($contentitem) {
4010 return $contentitem->favourite == true;
4011 }));
4012 $this->assertCount(1, $favourited);
4013 $this->assertEquals('assign', $favourited[0]->name);
4017 * Verify that content items can be removed from user favourites.
4019 public function test_remove_content_item_from_user_favourites() {
4020 $this->resetAfterTest();
4022 $course = $this->getDataGenerator()->create_course();
4023 $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
4024 $this->setUser($user);
4026 // Using the internal API, set a favourite for the user.
4027 $contentitemservice = new \core_course\local\service\content_item_service(
4028 new \core_course\local\repository\content_item_readonly_repository()
4030 $contentitems = $contentitemservice->get_all_content_items($user);
4031 $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))];
4032 $contentitemservice->add_to_user_favourites($user, $assign->componentname, $assign->id);
4034 $contentitems = $contentitemservice->get_all_content_items($user);
4035 $favourited = array_filter($contentitems, function($contentitem) {
4036 return $contentitem->favourite == true;
4038 $this->assertCount(1, $favourited);
4040 // Now, verify the external API can remove the favourite.
4041 $contentitem = core_course_external::remove_content_item_from_user_favourites('mod_assign', $assign->id);
4042 $contentitem = external_api::clean_returnvalue(core_course_external::remove_content_item_from_user_favourites_returns(),
4043 $contentitem);
4045 // Verify the returned item is a favourite.
4046 $this->assertFalse($contentitem['favourite']);
4048 // Using the internal API, confirm we see no favourite items.
4049 $contentitems = $contentitemservice->get_all_content_items($user);
4050 $favourited = array_filter($contentitems, function($contentitem) {
4051 return $contentitem->favourite == true;
4053 $this->assertCount(0, $favourited);
4057 * Test the web service returning course content items for inclusion in activity choosers, etc.
4059 public function test_get_course_content_items() {
4060 $this->resetAfterTest();
4062 $course = self::getDataGenerator()->create_course();
4063 $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
4065 // Fetch available content items as the editing teacher.
4066 $this->setUser($user);
4067 $result = core_course_external::get_course_content_items($course->id);
4068 $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result);
4070 $contentitemservice = new \core_course\local\service\content_item_service(
4071 new \core_course\local\repository\content_item_readonly_repository()
4074 // Check if the webservice returns exactly what the service defines, albeit in array form.
4075 $serviceitemsasarray = array_map(function($item) {
4076 return (array) $item;
4077 }, $contentitemservice->get_content_items_for_user_in_course($user, $course));
4079 $this->assertEquals($serviceitemsasarray, $result['content_items']);
4083 * Test the web service returning course content items, specifically in case where the user can't manage activities.
4085 public function test_get_course_content_items_no_permission_to_manage() {
4086 $this->resetAfterTest();
4088 $course = self::getDataGenerator()->create_course();
4089 $user = self::getDataGenerator()->create_and_enrol($course, 'student');
4091 // Fetch available content items as a student, who won't have the permission to manage activities.
4092 $this->setUser($user);
4093 $result = core_course_external::get_course_content_items($course->id);
4094 $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result);
4096 $this->assertEmpty($result['content_items']);
4100 * Test toggling the recommendation of an activity.
4102 public function test_toggle_activity_recommendation() {
4103 global $CFG;
4105 $this->resetAfterTest();
4107 $context = context_system::instance();
4108 $usercontext = context_user::instance($CFG->siteguest);
4109 $component = 'core_course';
4110 $favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext);
4112 $areaname = 'test_core';
4113 $areaid = 3;
4115 // Test we have the favourite.
4116 $this->setAdminUser();
4117 $result = core_course_external::toggle_activity_recommendation($areaname, $areaid);
4118 $this->assertTrue($favouritefactory->favourite_exists($component,
4119 \core_course\local\service\content_item_service::RECOMMENDATION_PREFIX . $areaname, $areaid, $context));
4120 $this->assertTrue($result['status']);
4121 // Test that it is now gone.
4122 $result = core_course_external::toggle_activity_recommendation($areaname, $areaid);
4123 $this->assertFalse($favouritefactory->favourite_exists($component, $areaname, $areaid, $context));
4124 $this->assertFalse($result['status']);