Merge branch 'MDL-64657-master' of git://github.com/jleyva/moodle into master
[moodle.git] / course / tests / externallib_test.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * External course functions unit tests
19  *
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
24  */
26 defined('MOODLE_INTERNAL') || die();
28 global $CFG;
30 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
32 /**
33  * External course functions unit tests
34  *
35  * @package    core_course
36  * @category   external
37  * @copyright  2012 Jerome Mouneyrac
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 class core_course_externallib_testcase extends externallib_advanced_testcase {
42     /**
43      * Tests set up
44      */
45     protected function setUp() {
46         global $CFG;
47         require_once($CFG->dirroot . '/course/externallib.php');
48     }
50     /**
51      * Test create_categories
52      */
53     public function test_create_categories() {
55         global $DB;
57         $this->resetAfterTest(true);
59         // Set the required capabilities by the external function
60         $contextid = context_system::instance()->id;
61         $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
63         // Create base categories.
64         $category1 = new stdClass();
65         $category1->name = 'Root Test Category 1';
66         $category2 = new stdClass();
67         $category2->name = 'Root Test Category 2';
68         $category2->idnumber = 'rootcattest2';
69         $category2->desc = 'Description for root test category 1';
70         $category2->theme = 'classic';
71         $categories = array(
72             array('name' => $category1->name, 'parent' => 0),
73             array('name' => $category2->name, 'parent' => 0, 'idnumber' => $category2->idnumber,
74                 'description' => $category2->desc, 'theme' => $category2->theme)
75         );
77         $createdcats = core_course_external::create_categories($categories);
79         // We need to execute the return values cleaning process to simulate the web service server.
80         $createdcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdcats);
82         // Initially confirm that base data was inserted correctly.
83         $this->assertEquals($category1->name, $createdcats[0]['name']);
84         $this->assertEquals($category2->name, $createdcats[1]['name']);
86         // Save the ids.
87         $category1->id = $createdcats[0]['id'];
88         $category2->id = $createdcats[1]['id'];
90         // Create on sub category.
91         $category3 = new stdClass();
92         $category3->name = 'Sub Root Test Category 3';
93         $subcategories = array(
94             array('name' => $category3->name, 'parent' => $category1->id)
95         );
97         $createdsubcats = core_course_external::create_categories($subcategories);
99         // We need to execute the return values cleaning process to simulate the web service server.
100         $createdsubcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdsubcats);
102         // Confirm that sub categories were inserted correctly.
103         $this->assertEquals($category3->name, $createdsubcats[0]['name']);
105         // Save the ids.
106         $category3->id = $createdsubcats[0]['id'];
108         // Calling the ws function should provide a new sortorder to give category1,
109         // category2, category3. New course categories are ordered by id not name.
110         $category1 = $DB->get_record('course_categories', array('id' => $category1->id));
111         $category2 = $DB->get_record('course_categories', array('id' => $category2->id));
112         $category3 = $DB->get_record('course_categories', array('id' => $category3->id));
114         // sortorder sequence (and sortorder) must be:
115         // category 1
116         //   category 3
117         // category 2
118         $this->assertGreaterThan($category1->sortorder, $category3->sortorder);
119         $this->assertGreaterThan($category3->sortorder, $category2->sortorder);
121         // Call without required capability
122         $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
123         $this->expectException('required_capability_exception');
124         $createdsubcats = core_course_external::create_categories($subcategories);
126     }
128     /**
129      * Test delete categories
130      */
131     public function test_delete_categories() {
132         global $DB;
134         $this->resetAfterTest(true);
136         // Set the required capabilities by the external function
137         $contextid = context_system::instance()->id;
138         $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
140         $category1  = self::getDataGenerator()->create_category();
141         $category2  = self::getDataGenerator()->create_category(
142                 array('parent' => $category1->id));
143         $category3  = self::getDataGenerator()->create_category();
144         $category4  = self::getDataGenerator()->create_category(
145                 array('parent' => $category3->id));
146         $category5  = self::getDataGenerator()->create_category(
147                 array('parent' => $category4->id));
149         //delete category 1 and 2 + delete category 4, category 5 moved under category 3
150         core_course_external::delete_categories(array(
151             array('id' => $category1->id, 'recursive' => 1),
152             array('id' => $category4->id)
153         ));
155         //check $category 1 and 2 are deleted
156         $notdeletedcount = $DB->count_records_select('course_categories',
157             'id IN ( ' . $category1->id . ',' . $category2->id . ',' . $category4->id . ')');
158         $this->assertEquals(0, $notdeletedcount);
160         //check that $category5 as $category3 for parent
161         $dbcategory5 = $DB->get_record('course_categories', array('id' => $category5->id));
162         $this->assertEquals($dbcategory5->path, $category3->path . '/' . $category5->id);
164          // Call without required capability
165         $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
166         $this->expectException('required_capability_exception');
167         $createdsubcats = core_course_external::delete_categories(
168                 array(array('id' => $category3->id)));
169     }
171     /**
172      * Test get categories
173      */
174     public function test_get_categories() {
175         global $DB;
177         $this->resetAfterTest(true);
179         $generatedcats = array();
180         $category1data['idnumber'] = 'idnumbercat1';
181         $category1data['name'] = 'Category 1 for PHPunit test';
182         $category1data['description'] = 'Category 1 description';
183         $category1data['descriptionformat'] = FORMAT_MOODLE;
184         $category1  = self::getDataGenerator()->create_category($category1data);
185         $generatedcats[$category1->id] = $category1;
186         $category2  = self::getDataGenerator()->create_category(
187                 array('parent' => $category1->id));
188         $generatedcats[$category2->id] = $category2;
189         $category6  = self::getDataGenerator()->create_category(
190                 array('parent' => $category1->id, 'visible' => 0));
191         $generatedcats[$category6->id] = $category6;
192         $category3  = self::getDataGenerator()->create_category();
193         $generatedcats[$category3->id] = $category3;
194         $category4  = self::getDataGenerator()->create_category(
195                 array('parent' => $category3->id));
196         $generatedcats[$category4->id] = $category4;
197         $category5  = self::getDataGenerator()->create_category(
198                 array('parent' => $category4->id));
199         $generatedcats[$category5->id] = $category5;
201         // Set the required capabilities by the external function.
202         $context = context_system::instance();
203         $roleid = $this->assignUserCapability('moodle/category:manage', $context->id);
204         $this->assignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
206         // Retrieve category1 + sub-categories except not visible ones
207         $categories = core_course_external::get_categories(array(
208             array('key' => 'id', 'value' => $category1->id),
209             array('key' => 'visible', 'value' => 1)), 1);
211         // We need to execute the return values cleaning process to simulate the web service server.
212         $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
214         // Check we retrieve the good total number of categories.
215         $this->assertEquals(2, count($categories));
217         // Check the return values
218         foreach ($categories as $category) {
219             $generatedcat = $generatedcats[$category['id']];
220             $this->assertEquals($category['idnumber'], $generatedcat->idnumber);
221             $this->assertEquals($category['name'], $generatedcat->name);
222             // Description was converted to the HTML format.
223             $this->assertEquals($category['description'], format_text($generatedcat->description, FORMAT_MOODLE, array('para' => false)));
224             $this->assertEquals($category['descriptionformat'], FORMAT_HTML);
225         }
227         // Check categories by ids.
228         $ids = implode(',', array_keys($generatedcats));
229         $categories = core_course_external::get_categories(array(
230             array('key' => 'ids', 'value' => $ids)), 0);
232         // We need to execute the return values cleaning process to simulate the web service server.
233         $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
235         // Check we retrieve the good total number of categories.
236         $this->assertEquals(6, count($categories));
237         // Check ids.
238         $returnedids = [];
239         foreach ($categories as $category) {
240             $returnedids[] = $category['id'];
241         }
242         // Sort the arrays upon comparision.
243         $this->assertEquals(array_keys($generatedcats), $returnedids, '', 0.0, 10, true);
245         // Check different params.
246         $categories = core_course_external::get_categories(array(
247             array('key' => 'id', 'value' => $category1->id),
248             array('key' => 'ids', 'value' => $category1->id),
249             array('key' => 'idnumber', 'value' => $category1->idnumber),
250             array('key' => 'visible', 'value' => 1)), 0);
252         // We need to execute the return values cleaning process to simulate the web service server.
253         $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
255         $this->assertEquals(1, count($categories));
257         // Same query, but forcing a parameters clean.
258         $categories = core_course_external::get_categories(array(
259             array('key' => 'id', 'value' => "$category1->id"),
260             array('key' => 'idnumber', 'value' => $category1->idnumber),
261             array('key' => 'name', 'value' => $category1->name . "<br/>"),
262             array('key' => 'visible', 'value' => '1')), 0);
263         $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
265         $this->assertEquals(1, count($categories));
267         // Retrieve categories from parent.
268         $categories = core_course_external::get_categories(array(
269             array('key' => 'parent', 'value' => $category3->id)), 1);
270         $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
272         $this->assertEquals(2, count($categories));
274         // Retrieve all categories.
275         $categories = core_course_external::get_categories();
277         // We need to execute the return values cleaning process to simulate the web service server.
278         $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
280         $this->assertEquals($DB->count_records('course_categories'), count($categories));
282         $this->unassignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
284         // Ensure maxdepthcategory is 2 and retrieve all categories without category:viewhiddencategories capability.
285         // It should retrieve all visible categories as well.
286         set_config('maxcategorydepth', 2);
287         $categories = core_course_external::get_categories();
289         // We need to execute the return values cleaning process to simulate the web service server.
290         $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
292         $this->assertEquals($DB->count_records('course_categories', array('visible' => 1)), count($categories));
294         // Call without required capability (it will fail cause of the search on idnumber).
295         $this->expectException('moodle_exception');
296         $categories = core_course_external::get_categories(array(
297             array('key' => 'id', 'value' => $category1->id),
298             array('key' => 'idnumber', 'value' => $category1->idnumber),
299             array('key' => 'visible', 'value' => 1)), 0);
300     }
302     /**
303      * Test update_categories
304      */
305     public function test_update_categories() {
306         global $DB;
308         $this->resetAfterTest(true);
310         // Set the required capabilities by the external function
311         $contextid = context_system::instance()->id;
312         $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
314         // Create base categories.
315         $category1data['idnumber'] = 'idnumbercat1';
316         $category1data['name'] = 'Category 1 for PHPunit test';
317         $category1data['description'] = 'Category 1 description';
318         $category1data['descriptionformat'] = FORMAT_MOODLE;
319         $category1  = self::getDataGenerator()->create_category($category1data);
320         $category2  = self::getDataGenerator()->create_category(
321                 array('parent' => $category1->id));
322         $category3  = self::getDataGenerator()->create_category();
323         $category4  = self::getDataGenerator()->create_category(
324                 array('parent' => $category3->id));
325         $category5  = self::getDataGenerator()->create_category(
326                 array('parent' => $category4->id));
328         // We update all category1 attribut.
329         // Then we move cat4 and cat5 parent: cat3 => cat1
330         $categories = array(
331             array('id' => $category1->id,
332                 'name' => $category1->name . '_updated',
333                 'idnumber' => $category1->idnumber . '_updated',
334                 'description' => $category1->description . '_updated',
335                 'descriptionformat' => FORMAT_HTML,
336                 'theme' => $category1->theme),
337             array('id' => $category4->id, 'parent' => $category1->id));
339         core_course_external::update_categories($categories);
341         // Check the values were updated.
342         $dbcategories = $DB->get_records_select('course_categories',
343                 'id IN (' . $category1->id . ',' . $category2->id . ',' . $category2->id
344                 . ',' . $category3->id . ',' . $category4->id . ',' . $category5->id .')');
345         $this->assertEquals($category1->name . '_updated',
346                 $dbcategories[$category1->id]->name);
347         $this->assertEquals($category1->idnumber . '_updated',
348                 $dbcategories[$category1->id]->idnumber);
349         $this->assertEquals($category1->description . '_updated',
350                 $dbcategories[$category1->id]->description);
351         $this->assertEquals(FORMAT_HTML, $dbcategories[$category1->id]->descriptionformat);
353         // Check that category4 and category5 have been properly moved.
354         $this->assertEquals('/' . $category1->id . '/' . $category4->id,
355                 $dbcategories[$category4->id]->path);
356         $this->assertEquals('/' . $category1->id . '/' . $category4->id . '/' . $category5->id,
357                 $dbcategories[$category5->id]->path);
359         // Call without required capability.
360         $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
361         $this->expectException('required_capability_exception');
362         core_course_external::update_categories($categories);
363     }
365     /**
366      * Test create_courses numsections
367      */
368     public function test_create_course_numsections() {
369         global $DB;
371         $this->resetAfterTest(true);
373         // Set the required capabilities by the external function.
374         $contextid = context_system::instance()->id;
375         $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
376         $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
378         $numsections = 10;
379         $category  = self::getDataGenerator()->create_category();
381         // Create base categories.
382         $course1['fullname'] = 'Test course 1';
383         $course1['shortname'] = 'Testcourse1';
384         $course1['categoryid'] = $category->id;
385         $course1['courseformatoptions'][] = array('name' => 'numsections', 'value' => $numsections);
387         $courses = array($course1);
389         $createdcourses = core_course_external::create_courses($courses);
390         foreach ($createdcourses as $createdcourse) {
391             $existingsections = $DB->get_records('course_sections', array('course' => $createdcourse['id']));
392             $modinfo = get_fast_modinfo($createdcourse['id']);
393             $sections = $modinfo->get_section_info_all();
394             $this->assertEquals(count($sections), $numsections + 1); // Includes generic section.
395             $this->assertEquals(count($existingsections), $numsections + 1); // Includes generic section.
396         }
397     }
399     /**
400      * Test create_courses
401      */
402     public function test_create_courses() {
403         global $DB;
405         $this->resetAfterTest(true);
407         // Enable course completion.
408         set_config('enablecompletion', 1);
409         // Enable course themes.
410         set_config('allowcoursethemes', 1);
412         // Custom fields.
413         $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
415         $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
416             'categoryid' => $fieldcategory->get('id'),
417             'configdata' => ['visibility' => \core_course\customfield\course_handler::VISIBLETOALL]];
418         $field = self::getDataGenerator()->create_custom_field($customfield);
420         // Set the required capabilities by the external function
421         $contextid = context_system::instance()->id;
422         $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
423         $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
424         $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
426         $category  = self::getDataGenerator()->create_category();
428         // Create base categories.
429         $course1['fullname'] = 'Test course 1';
430         $course1['shortname'] = 'Testcourse1';
431         $course1['categoryid'] = $category->id;
432         $course2['fullname'] = 'Test course 2';
433         $course2['shortname'] = 'Testcourse2';
434         $course2['categoryid'] = $category->id;
435         $course2['idnumber'] = 'testcourse2idnumber';
436         $course2['summary'] = 'Description for course 2';
437         $course2['summaryformat'] = FORMAT_MOODLE;
438         $course2['format'] = 'weeks';
439         $course2['showgrades'] = 1;
440         $course2['newsitems'] = 3;
441         $course2['startdate'] = 1420092000; // 01/01/2015.
442         $course2['enddate'] = 1422669600; // 01/31/2015.
443         $course2['numsections'] = 4;
444         $course2['maxbytes'] = 100000;
445         $course2['showreports'] = 1;
446         $course2['visible'] = 0;
447         $course2['hiddensections'] = 0;
448         $course2['groupmode'] = 0;
449         $course2['groupmodeforce'] = 0;
450         $course2['defaultgroupingid'] = 0;
451         $course2['enablecompletion'] = 1;
452         $course2['completionnotify'] = 1;
453         $course2['lang'] = 'en';
454         $course2['forcetheme'] = 'classic';
455         $course2['courseformatoptions'][] = array('name' => 'automaticenddate', 'value' => 0);
456         $course3['fullname'] = 'Test course 3';
457         $course3['shortname'] = 'Testcourse3';
458         $course3['categoryid'] = $category->id;
459         $course3['format'] = 'topics';
460         $course3options = array('numsections' => 8,
461             'hiddensections' => 1,
462             'coursedisplay' => 1);
463         $course3['courseformatoptions'] = array();
464         foreach ($course3options as $key => $value) {
465             $course3['courseformatoptions'][] = array('name' => $key, 'value' => $value);
466         }
467         $course4['fullname'] = 'Test course with custom fields';
468         $course4['shortname'] = 'Testcoursecustomfields';
469         $course4['categoryid'] = $category->id;
470         $course4['customfields'] = [['shortname' => $customfield['shortname'], 'value' => 'Test value']];
471         $courses = array($course4, $course1, $course2, $course3);
473         $createdcourses = core_course_external::create_courses($courses);
475         // We need to execute the return values cleaning process to simulate the web service server.
476         $createdcourses = external_api::clean_returnvalue(core_course_external::create_courses_returns(), $createdcourses);
478         // Check that right number of courses were created.
479         $this->assertEquals(4, count($createdcourses));
481         // Check that the courses were correctly created.
482         foreach ($createdcourses as $createdcourse) {
483             $courseinfo = course_get_format($createdcourse['id'])->get_course();
485             if ($createdcourse['shortname'] == $course2['shortname']) {
486                 $this->assertEquals($courseinfo->fullname, $course2['fullname']);
487                 $this->assertEquals($courseinfo->shortname, $course2['shortname']);
488                 $this->assertEquals($courseinfo->category, $course2['categoryid']);
489                 $this->assertEquals($courseinfo->idnumber, $course2['idnumber']);
490                 $this->assertEquals($courseinfo->summary, $course2['summary']);
491                 $this->assertEquals($courseinfo->summaryformat, $course2['summaryformat']);
492                 $this->assertEquals($courseinfo->format, $course2['format']);
493                 $this->assertEquals($courseinfo->showgrades, $course2['showgrades']);
494                 $this->assertEquals($courseinfo->newsitems, $course2['newsitems']);
495                 $this->assertEquals($courseinfo->startdate, $course2['startdate']);
496                 $this->assertEquals($courseinfo->enddate, $course2['enddate']);
497                 $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(), $course2['numsections']);
498                 $this->assertEquals($courseinfo->maxbytes, $course2['maxbytes']);
499                 $this->assertEquals($courseinfo->showreports, $course2['showreports']);
500                 $this->assertEquals($courseinfo->visible, $course2['visible']);
501                 $this->assertEquals($courseinfo->hiddensections, $course2['hiddensections']);
502                 $this->assertEquals($courseinfo->groupmode, $course2['groupmode']);
503                 $this->assertEquals($courseinfo->groupmodeforce, $course2['groupmodeforce']);
504                 $this->assertEquals($courseinfo->defaultgroupingid, $course2['defaultgroupingid']);
505                 $this->assertEquals($courseinfo->completionnotify, $course2['completionnotify']);
506                 $this->assertEquals($courseinfo->lang, $course2['lang']);
507                 $this->assertEquals($courseinfo->theme, $course2['forcetheme']);
509                 // We enabled completion at the beginning of the test.
510                 $this->assertEquals($courseinfo->enablecompletion, $course2['enablecompletion']);
512             } else if ($createdcourse['shortname'] == $course1['shortname']) {
513                 $courseconfig = get_config('moodlecourse');
514                 $this->assertEquals($courseinfo->fullname, $course1['fullname']);
515                 $this->assertEquals($courseinfo->shortname, $course1['shortname']);
516                 $this->assertEquals($courseinfo->category, $course1['categoryid']);
517                 $this->assertEquals($courseinfo->summaryformat, FORMAT_HTML);
518                 $this->assertEquals($courseinfo->format, $courseconfig->format);
519                 $this->assertEquals($courseinfo->showgrades, $courseconfig->showgrades);
520                 $this->assertEquals($courseinfo->newsitems, $courseconfig->newsitems);
521                 $this->assertEquals($courseinfo->maxbytes, $courseconfig->maxbytes);
522                 $this->assertEquals($courseinfo->showreports, $courseconfig->showreports);
523                 $this->assertEquals($courseinfo->groupmode, $courseconfig->groupmode);
524                 $this->assertEquals($courseinfo->groupmodeforce, $courseconfig->groupmodeforce);
525                 $this->assertEquals($courseinfo->defaultgroupingid, 0);
526             } else if ($createdcourse['shortname'] == $course3['shortname']) {
527                 $this->assertEquals($courseinfo->fullname, $course3['fullname']);
528                 $this->assertEquals($courseinfo->shortname, $course3['shortname']);
529                 $this->assertEquals($courseinfo->category, $course3['categoryid']);
530                 $this->assertEquals($courseinfo->format, $course3['format']);
531                 $this->assertEquals($courseinfo->hiddensections, $course3options['hiddensections']);
532                 $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(),
533                     $course3options['numsections']);
534                 $this->assertEquals($courseinfo->coursedisplay, $course3options['coursedisplay']);
535             } else if ($createdcourse['shortname'] == $course4['shortname']) {
536                 $this->assertEquals($courseinfo->fullname, $course4['fullname']);
537                 $this->assertEquals($courseinfo->shortname, $course4['shortname']);
538                 $this->assertEquals($courseinfo->category, $course4['categoryid']);
540                 $handler = core_course\customfield\course_handler::create();
541                 $customfields = $handler->export_instance_data_object($createdcourse['id']);
542                 $this->assertEquals((object)['test' => 'Test value'], $customfields);
543             } else {
544                 throw new moodle_exception('Unexpected shortname');
545             }
546         }
548         // Call without required capability
549         $this->unassignUserCapability('moodle/course:create', $contextid, $roleid);
550         $this->expectException('required_capability_exception');
551         $createdsubcats = core_course_external::create_courses($courses);
552     }
554     /**
555      * Data provider for testing empty fields produce expected exceptions
556      *
557      * @see test_create_courses_empty_field
558      * @see test_update_courses_empty_field
559      *
560      * @return array
561      */
562     public function course_empty_field_provider(): array {
563         return [
564             [[
565                 'fullname' => '',
566                 'shortname' => 'ws101',
567             ], 'fullname'],
568             [[
569                 'fullname' => ' ',
570                 'shortname' => 'ws101',
571             ], 'fullname'],
572             [[
573                 'fullname' => 'Web Services',
574                 'shortname' => '',
575             ], 'shortname'],
576             [[
577                 'fullname' => 'Web Services',
578                 'shortname' => ' ',
579             ], 'shortname'],
580         ];
581     }
583     /**
584      * Test creating courses with empty fields throws an exception
585      *
586      * @param array $course
587      * @param string $expectedemptyfield
588      *
589      * @dataProvider course_empty_field_provider
590      */
591     public function test_create_courses_empty_field(array $course, string $expectedemptyfield): void {
592         $this->resetAfterTest();
593         $this->setAdminUser();
595         // Create a category for the new course.
596         $course['categoryid'] = $this->getDataGenerator()->create_category()->id;
598         $this->expectException(moodle_exception::class);
599         $this->expectExceptionMessageRegExp("/{$expectedemptyfield}/");
600         core_course_external::create_courses([$course]);
601     }
603     /**
604      * Test updating courses with empty fields returns warnings
605      *
606      * @param array $course
607      * @param string $expectedemptyfield
608      *
609      * @dataProvider course_empty_field_provider
610      */
611     public function test_update_courses_empty_field(array $course, string $expectedemptyfield): void {
612         $this->resetAfterTest();
613         $this->setAdminUser();
615         // Create a course to update.
616         $course['id'] = $this->getDataGenerator()->create_course()->id;
618         $result = core_course_external::update_courses([$course]);
619         $result = core_course_external::clean_returnvalue(core_course_external::update_courses_returns(), $result);
621         $this->assertCount(1, $result['warnings']);
623         $warning = reset($result['warnings']);
624         $this->assertEquals('errorinvalidparam', $warning['warningcode']);
625         $this->assertContains($expectedemptyfield, $warning['message']);
626     }
628     /**
629      * Test delete_courses
630      */
631     public function test_delete_courses() {
632         global $DB, $USER;
634         $this->resetAfterTest(true);
636         // Admin can delete a course.
637         $this->setAdminUser();
638         // Validate_context() will fail as the email is not set by $this->setAdminUser().
639         $USER->email = 'emailtopass@example.com';
641         $course1  = self::getDataGenerator()->create_course();
642         $course2  = self::getDataGenerator()->create_course();
643         $course3  = self::getDataGenerator()->create_course();
645         // Delete courses.
646         $result = core_course_external::delete_courses(array($course1->id, $course2->id));
647         $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
648         // Check for 0 warnings.
649         $this->assertEquals(0, count($result['warnings']));
651         // Check $course 1 and 2 are deleted.
652         $notdeletedcount = $DB->count_records_select('course',
653             'id IN ( ' . $course1->id . ',' . $course2->id . ')');
654         $this->assertEquals(0, $notdeletedcount);
656         // Try to delete non-existent course.
657         $result = core_course_external::delete_courses(array($course1->id));
658         $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
659         // Check for 1 warnings.
660         $this->assertEquals(1, count($result['warnings']));
662         // Try to delete Frontpage course.
663         $result = core_course_external::delete_courses(array(0));
664         $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
665         // Check for 1 warnings.
666         $this->assertEquals(1, count($result['warnings']));
668          // Fail when the user has access to course (enrolled) but does not have permission or is not admin.
669         $student1 = self::getDataGenerator()->create_user();
670         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
671         $this->getDataGenerator()->enrol_user($student1->id,
672                                               $course3->id,
673                                               $studentrole->id);
674         $this->setUser($student1);
675         $result = core_course_external::delete_courses(array($course3->id));
676         $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
677         // Check for 1 warnings.
678         $this->assertEquals(1, count($result['warnings']));
680          // Fail when the user is not allow to access the course (enrolled) or is not admin.
681         $this->setGuestUser();
682         $this->expectException('require_login_exception');
684         $result = core_course_external::delete_courses(array($course3->id));
685         $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
686     }
688     /**
689      * Test get_courses
690      */
691     public function test_get_courses () {
692         global $DB;
694         $this->resetAfterTest(true);
696         $generatedcourses = array();
697         $coursedata['idnumber'] = 'idnumbercourse1';
698         // Adding tags here to check that format_string is applied.
699         $coursedata['fullname'] = '<b>Course 1 for PHPunit test</b>';
700         $coursedata['shortname'] = '<b>Course 1 for PHPunit test</b>';
701         $coursedata['summary'] = 'Course 1 description';
702         $coursedata['summaryformat'] = FORMAT_MOODLE;
703         $course1  = self::getDataGenerator()->create_course($coursedata);
705         $fieldcategory = self::getDataGenerator()->create_custom_field_category(
706             ['name' => 'Other fields']);
708         $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
709             'categoryid' => $fieldcategory->get('id')];
710         $field = self::getDataGenerator()->create_custom_field($customfield);
712         $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
714         $generatedcourses[$course1->id] = $course1;
715         $course2  = self::getDataGenerator()->create_course();
716         $generatedcourses[$course2->id] = $course2;
717         $course3  = self::getDataGenerator()->create_course(array('format' => 'topics'));
718         $generatedcourses[$course3->id] = $course3;
719         $course4  = self::getDataGenerator()->create_course(['customfields' => [$customfieldvalue]]);
720         $generatedcourses[$course4->id] = $course4;
722         // Set the required capabilities by the external function.
723         $context = context_system::instance();
724         $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
725         $this->assignUserCapability('moodle/course:update',
726                 context_course::instance($course1->id)->id, $roleid);
727         $this->assignUserCapability('moodle/course:update',
728                 context_course::instance($course2->id)->id, $roleid);
729         $this->assignUserCapability('moodle/course:update',
730                 context_course::instance($course3->id)->id, $roleid);
731         $this->assignUserCapability('moodle/course:update',
732                 context_course::instance($course4->id)->id, $roleid);
734         $courses = core_course_external::get_courses(array('ids' =>
735             array($course1->id, $course2->id, $course4->id)));
737         // We need to execute the return values cleaning process to simulate the web service server.
738         $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
740         // Check we retrieve the good total number of courses.
741         $this->assertEquals(3, count($courses));
743         foreach ($courses as $course) {
744             $coursecontext = context_course::instance($course['id']);
745             $dbcourse = $generatedcourses[$course['id']];
746             $this->assertEquals($course['idnumber'], $dbcourse->idnumber);
747             $this->assertEquals($course['fullname'], external_format_string($dbcourse->fullname, $coursecontext->id));
748             $this->assertEquals($course['displayname'], external_format_string(get_course_display_name_for_list($dbcourse),
749                 $coursecontext->id));
750             // Summary was converted to the HTML format.
751             $this->assertEquals($course['summary'], format_text($dbcourse->summary, FORMAT_MOODLE, array('para' => false)));
752             $this->assertEquals($course['summaryformat'], FORMAT_HTML);
753             $this->assertEquals($course['shortname'], external_format_string($dbcourse->shortname, $coursecontext->id));
754             $this->assertEquals($course['categoryid'], $dbcourse->category);
755             $this->assertEquals($course['format'], $dbcourse->format);
756             $this->assertEquals($course['showgrades'], $dbcourse->showgrades);
757             $this->assertEquals($course['newsitems'], $dbcourse->newsitems);
758             $this->assertEquals($course['startdate'], $dbcourse->startdate);
759             $this->assertEquals($course['enddate'], $dbcourse->enddate);
760             $this->assertEquals($course['numsections'], course_get_format($dbcourse)->get_last_section_number());
761             $this->assertEquals($course['maxbytes'], $dbcourse->maxbytes);
762             $this->assertEquals($course['showreports'], $dbcourse->showreports);
763             $this->assertEquals($course['visible'], $dbcourse->visible);
764             $this->assertEquals($course['hiddensections'], $dbcourse->hiddensections);
765             $this->assertEquals($course['groupmode'], $dbcourse->groupmode);
766             $this->assertEquals($course['groupmodeforce'], $dbcourse->groupmodeforce);
767             $this->assertEquals($course['defaultgroupingid'], $dbcourse->defaultgroupingid);
768             $this->assertEquals($course['completionnotify'], $dbcourse->completionnotify);
769             $this->assertEquals($course['lang'], $dbcourse->lang);
770             $this->assertEquals($course['forcetheme'], $dbcourse->theme);
771             $this->assertEquals($course['enablecompletion'], $dbcourse->enablecompletion);
772             if ($dbcourse->format === 'topics') {
773                 $this->assertEquals($course['courseformatoptions'], array(
774                     array('name' => 'hiddensections', 'value' => $dbcourse->hiddensections),
775                     array('name' => 'coursedisplay', 'value' => $dbcourse->coursedisplay),
776                 ));
777             }
779             // Assert custom field that we previously added to test course 4.
780             if ($dbcourse->id == $course4->id) {
781                 $this->assertEquals([
782                     'shortname' => $customfield['shortname'],
783                     'name' => $customfield['name'],
784                     'type' => $customfield['type'],
785                     'value' => $customfieldvalue['value'],
786                     'valueraw' => $customfieldvalue['value'],
787                 ], $course['customfields'][0]);
788             }
789         }
791         // Get all courses in the DB
792         $courses = core_course_external::get_courses(array());
794         // We need to execute the return values cleaning process to simulate the web service server.
795         $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
797         $this->assertEquals($DB->count_records('course'), count($courses));
798     }
800     /**
801      * Test retrieving courses returns custom field data
802      */
803     public function test_get_courses_customfields(): void {
804         $this->resetAfterTest();
805         $this->setAdminUser();
807         $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
808         $datefield = $this->getDataGenerator()->create_custom_field([
809             'categoryid' => $fieldcategory->get('id'),
810             'shortname' => 'mydate',
811             'name' => 'My date',
812             'type' => 'date',
813         ]);
815         $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
816             [
817                 'shortname' => $datefield->get('shortname'),
818                 'value' => 1580389200, // 30/01/2020 13:00 GMT.
819             ],
820         ]]);
822         $courses = external_api::clean_returnvalue(
823             core_course_external::get_courses_returns(),
824             core_course_external::get_courses(['ids' => [$newcourse->id]])
825         );
827         $this->assertCount(1, $courses);
828         $course = reset($courses);
830         $this->assertArrayHasKey('customfields', $course);
831         $this->assertCount(1, $course['customfields']);
833         // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
834         $this->assertEquals([
835             'name' => $datefield->get('name'),
836             'shortname' => $datefield->get('shortname'),
837             'type' => $datefield->get('type'),
838             'value' => userdate(1580389200),
839             'valueraw' => 1580389200,
840         ], reset($course['customfields']));
841     }
843     /**
844      * Test get_courses without capability
845      */
846     public function test_get_courses_without_capability() {
847         $this->resetAfterTest(true);
849         $course1 = $this->getDataGenerator()->create_course();
850         $this->setUser($this->getDataGenerator()->create_user());
852         // No permissions are required to get the site course.
853         $courses = core_course_external::get_courses(array('ids' => [SITEID]));
854         $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
856         $this->assertEquals(1, count($courses));
857         $this->assertEquals('PHPUnit test site', $courses[0]['fullname']);
858         $this->assertEquals('site', $courses[0]['format']);
860         // Requesting course without being enrolled or capability to view it will throw an exception.
861         try {
862             core_course_external::get_courses(array('ids' => [$course1->id]));
863             $this->fail('Exception expected');
864         } catch (moodle_exception $e) {
865             $this->assertEquals(1, preg_match('/Course or activity not accessible. \(Not enrolled\)/', $e->getMessage()));
866         }
867     }
869     /**
870      * Test search_courses
871      */
872     public function test_search_courses () {
874         global $DB;
876         $this->resetAfterTest(true);
877         $this->setAdminUser();
878         $generatedcourses = array();
879         $coursedata1['fullname'] = 'FIRST COURSE';
880         $course1  = self::getDataGenerator()->create_course($coursedata1);
882         $page = new moodle_page();
883         $page->set_course($course1);
884         $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
886         $coursedata2['fullname'] = 'SECOND COURSE';
887         $course2  = self::getDataGenerator()->create_course($coursedata2);
889         $page = new moodle_page();
890         $page->set_course($course2);
891         $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
893         // Search by name.
894         $results = core_course_external::search_courses('search', 'FIRST');
895         $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
896         $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
897         $this->assertCount(1, $results['courses']);
899         // Create the forum.
900         $record = new stdClass();
901         $record->introformat = FORMAT_HTML;
902         $record->course = $course2->id;
903         // Set Aggregate type = Average of ratings.
904         $forum = self::getDataGenerator()->create_module('forum', $record);
906         // Search by module.
907         $results = core_course_external::search_courses('modulelist', 'forum');
908         $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
909         $this->assertEquals(1, $results['total']);
911         // Enable coursetag option.
912         set_config('block_tags_showcoursetags', true);
913         // Add tag 'TAG-LABEL ON SECOND COURSE' to Course2.
914         core_tag_tag::set_item_tags('core', 'course', $course2->id, context_course::instance($course2->id),
915                 array('TAG-LABEL ON SECOND COURSE'));
916         $taginstance = $DB->get_record('tag_instance',
917                 array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST);
919         // Search by tagid.
920         $results = core_course_external::search_courses('tagid', $taginstance->tagid);
921         $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
922         $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
924         // Search by block (use news_items default block).
925         $blockid = $DB->get_field('block', 'id', array('name' => 'news_items'));
926         $results = core_course_external::search_courses('blocklist', $blockid);
927         $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
928         $this->assertEquals(2, $results['total']);
930         // Now as a normal user.
931         $user = self::getDataGenerator()->create_user();
933         // Add a 3rd, hidden, course we shouldn't see, even when enrolled as student.
934         $coursedata3['fullname'] = 'HIDDEN COURSE';
935         $coursedata3['visible'] = 0;
936         $course3  = self::getDataGenerator()->create_course($coursedata3);
937         $this->getDataGenerator()->enrol_user($user->id, $course3->id, 'student');
939         $this->getDataGenerator()->enrol_user($user->id, $course2->id, 'student');
940         $this->setUser($user);
942         $results = core_course_external::search_courses('search', 'FIRST');
943         $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
944         $this->assertCount(1, $results['courses']);
945         $this->assertEquals(1, $results['total']);
946         $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
948         // Check that we can see all courses without the limit to enrolled setting.
949         $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 0);
950         $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
951         $this->assertCount(2, $results['courses']);
952         $this->assertEquals(2, $results['total']);
954         // Check that we only see our enrolled course when limiting.
955         $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 1);
956         $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
957         $this->assertCount(1, $results['courses']);
958         $this->assertEquals(1, $results['total']);
959         $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
961         // Search by block (use news_items default block). Should fail (only admins allowed).
962         $this->expectException('required_capability_exception');
963         $results = core_course_external::search_courses('blocklist', $blockid);
964     }
966     /**
967      * Test searching for courses returns custom field data
968      */
969     public function test_search_courses_customfields(): void {
970         $this->resetAfterTest();
971         $this->setAdminUser();
973         $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
974         $datefield = $this->getDataGenerator()->create_custom_field([
975             'categoryid' => $fieldcategory->get('id'),
976             'shortname' => 'mydate',
977             'name' => 'My date',
978             'type' => 'date',
979         ]);
981         $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
982             [
983                 'shortname' => $datefield->get('shortname'),
984                 'value' => 1580389200, // 30/01/2020 13:00 GMT.
985             ],
986         ]]);
988         $result = external_api::clean_returnvalue(
989             core_course_external::search_courses_returns(),
990             core_course_external::search_courses('search', $newcourse->shortname)
991         );
993         $this->assertCount(1, $result['courses']);
994         $course = reset($result['courses']);
996         $this->assertArrayHasKey('customfields', $course);
997         $this->assertCount(1, $course['customfields']);
999         // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
1000         $this->assertEquals([
1001             'name' => $datefield->get('name'),
1002             'shortname' => $datefield->get('shortname'),
1003             'type' => $datefield->get('type'),
1004             'value' => userdate(1580389200),
1005             'valueraw' => 1580389200,
1006         ], reset($course['customfields']));
1007     }
1009     /**
1010      * Create a course with contents
1011      * @return array A list with the course object and course modules objects
1012      */
1013     private function prepare_get_course_contents_test() {
1014         global $DB, $CFG;
1016         $CFG->allowstealth = 1; // Allow stealth activities.
1017         $CFG->enablecompletion = true;
1018         // Course with 4 sections (apart from the main section), with completion and not displaying hidden sections.
1019         $course  = self::getDataGenerator()->create_course(['numsections' => 4, 'enablecompletion' => 1, 'hiddensections' => 1]);
1021         $forumdescription = 'This is the forum description';
1022         $forum = $this->getDataGenerator()->create_module('forum',
1023             array('course' => $course->id, 'intro' => $forumdescription, 'trackingtype' => 2),
1024             array('showdescription' => true, 'completion' => COMPLETION_TRACKING_MANUAL));
1025         $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1026         // Add discussions to the tracking forced forum.
1027         $record = new stdClass();
1028         $record->course = $course->id;
1029         $record->userid = 0;
1030         $record->forum = $forum->id;
1031         $discussionforce = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1032         $data = $this->getDataGenerator()->create_module('data',
1033             array('assessed' => 1, 'scale' => 100, 'course' => $course->id, 'completion' => 2, 'completionentries' => 3));
1034         $datacm = get_coursemodule_from_instance('data', $data->id);
1035         $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
1036         $pagecm = get_coursemodule_from_instance('page', $page->id);
1037         // This is an stealth page (set by visibleoncoursepage).
1038         $pagestealth = $this->getDataGenerator()->create_module('page', array('course' => $course->id, 'visibleoncoursepage' => 0));
1039         $labeldescription = 'This is a very long label to test if more than 50 characters are returned.
1040                 So bla bla bla bla <b>bold bold bold</b> bla bla bla bla.';
1041         $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
1042             'intro' => $labeldescription, 'completion' => COMPLETION_TRACKING_MANUAL));
1043         $labelcm = get_coursemodule_from_instance('label', $label->id);
1044         $tomorrow = time() + DAYSECS;
1045         // Module with availability restrictions not met.
1046         $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '},'
1047                 .'{"type":"completion","cm":' . $label->cmid .',"e":1}],"showc":[true,true]}';
1048         $url = $this->getDataGenerator()->create_module('url',
1049             array('course' => $course->id, 'name' => 'URL: % & $ ../', 'section' => 2, 'display' => RESOURCELIB_DISPLAY_POPUP,
1050                 'popupwidth' => 100, 'popupheight' => 100),
1051             array('availability' => $availability));
1052         $urlcm = get_coursemodule_from_instance('url', $url->id);
1053         // Module for the last section.
1054         $this->getDataGenerator()->create_module('url',
1055             array('course' => $course->id, 'name' => 'URL for last section', 'section' => 3));
1056         // Module for section 1 with availability restrictions met.
1057         $yesterday = time() - DAYSECS;
1058         $this->getDataGenerator()->create_module('url',
1059             array('course' => $course->id, 'name' => 'URL restrictions met', 'section' => 1),
1060             array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":'. $yesterday .'}],"showc":[true]}'));
1062         // Set the required capabilities by the external function.
1063         $context = context_course::instance($course->id);
1064         $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
1065         $this->assignUserCapability('moodle/course:update', $context->id, $roleid);
1066         $this->assignUserCapability('mod/data:view', $context->id, $roleid);
1068         $conditions = array('course' => $course->id, 'section' => 2);
1069         $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
1071         // Add date availability condition not met for section 3.
1072         $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}';
1073         $DB->set_field('course_sections', 'availability', $availability,
1074                 array('course' => $course->id, 'section' => 3));
1076         // Create resource for last section.
1077         $pageinhiddensection = $this->getDataGenerator()->create_module('page',
1078             array('course' => $course->id, 'name' => 'Page in hidden section', 'section' => 4));
1079         // Set not visible last section.
1080         $DB->set_field('course_sections', 'visible', 0,
1081                 array('course' => $course->id, 'section' => 4));
1083         rebuild_course_cache($course->id, true);
1085         return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm);
1086     }
1088     /**
1089      * Test get_course_contents
1090      */
1091     public function test_get_course_contents() {
1092         global $CFG;
1093         $this->resetAfterTest(true);
1095         $CFG->forum_allowforcedreadtracking = 1;
1096         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1098         // We first run the test as admin.
1099         $this->setAdminUser();
1100         $sections = core_course_external::get_course_contents($course->id, array());
1101         // We need to execute the return values cleaning process to simulate the web service server.
1102         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1104         $modinfo = get_fast_modinfo($course);
1105         $testexecuted = 0;
1106         foreach ($sections[0]['modules'] as $module) {
1107             if ($module['id'] == $forumcm->id and $module['modname'] == 'forum') {
1108                 $cm = $modinfo->cms[$forumcm->id];
1109                 $formattedtext = format_text($cm->content, FORMAT_HTML,
1110                     array('noclean' => true, 'para' => false, 'filter' => false));
1111                 $this->assertEquals($formattedtext, $module['description']);
1112                 $this->assertEquals($forumcm->instance, $module['instance']);
1113                 $this->assertEquals(context_module::instance($forumcm->id)->id, $module['contextid']);
1114                 $this->assertContains('1 unread post', $module['afterlink']);
1115                 $this->assertFalse($module['noviewlink']);
1116                 $this->assertNotEmpty($module['description']);  // Module showdescription is on.
1117                 $testexecuted = $testexecuted + 2;
1118             } else if ($module['id'] == $labelcm->id and $module['modname'] == 'label') {
1119                 $cm = $modinfo->cms[$labelcm->id];
1120                 $formattedtext = format_text($cm->content, FORMAT_HTML,
1121                     array('noclean' => true, 'para' => false, 'filter' => false));
1122                 $this->assertEquals($formattedtext, $module['description']);
1123                 $this->assertEquals($labelcm->instance, $module['instance']);
1124                 $this->assertEquals(context_module::instance($labelcm->id)->id, $module['contextid']);
1125                 $this->assertTrue($module['noviewlink']);
1126                 $this->assertNotEmpty($module['description']);  // Label always prints the description.
1127                 $testexecuted = $testexecuted + 1;
1128             } else if ($module['id'] == $datacm->id and $module['modname'] == 'data') {
1129                 $this->assertContains('customcompletionrules', $module['customdata']);
1130                 $this->assertFalse($module['noviewlink']);
1131                 $this->assertArrayNotHasKey('description', $module);
1132                 $testexecuted = $testexecuted + 1;
1133             }
1134         }
1135         foreach ($sections[2]['modules'] as $module) {
1136             if ($module['id'] == $urlcm->id and $module['modname'] == 'url') {
1137                 $this->assertContains('width=100,height=100', $module['onclick']);
1138                 $testexecuted = $testexecuted + 1;
1139             }
1140         }
1142         $CFG->forum_allowforcedreadtracking = 0;    // Recover original value.
1143         forum_tp_count_forum_unread_posts($forumcm, $course, true);    // Reset static cache for further tests.
1145         $this->assertEquals(5, $testexecuted);
1146         $this->assertEquals(0, $sections[0]['section']);
1148         $this->assertCount(5, $sections[0]['modules']);
1149         $this->assertCount(1, $sections[1]['modules']);
1150         $this->assertCount(1, $sections[2]['modules']);
1151         $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions.
1152         $this->assertCount(1, $sections[4]['modules']); // One module for the hidden section with a visible activity.
1153         $this->assertNotEmpty($sections[3]['availabilityinfo']);
1154         $this->assertEquals(1, $sections[1]['section']);
1155         $this->assertEquals(2, $sections[2]['section']);
1156         $this->assertEquals(3, $sections[3]['section']);
1157         $this->assertEquals(4, $sections[4]['section']);
1158         $this->assertContains('<iframe', $sections[2]['summary']);
1159         $this->assertContains('</iframe>', $sections[2]['summary']);
1160         $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']);
1161         try {
1162             $sections = core_course_external::get_course_contents($course->id,
1163                                                                     array(array("name" => "invalid", "value" => 1)));
1164             $this->fail('Exception expected due to invalid option.');
1165         } catch (moodle_exception $e) {
1166             $this->assertEquals('errorinvalidparam', $e->errorcode);
1167         }
1168     }
1171     /**
1172      * Test get_course_contents as student
1173      */
1174     public function test_get_course_contents_student() {
1175         global $DB;
1176         $this->resetAfterTest(true);
1178         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1180         $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
1181         $user = self::getDataGenerator()->create_user();
1182         self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
1183         $this->setUser($user);
1185         $sections = core_course_external::get_course_contents($course->id, array());
1186         // We need to execute the return values cleaning process to simulate the web service server.
1187         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1189         $this->assertCount(4, $sections); // Nothing for the not visible section.
1190         $this->assertCount(5, $sections[0]['modules']);
1191         $this->assertCount(1, $sections[1]['modules']);
1192         $this->assertCount(1, $sections[2]['modules']);
1193         $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1195         $this->assertNotEmpty($sections[3]['availabilityinfo']);
1196         $this->assertEquals(1, $sections[1]['section']);
1197         $this->assertEquals(2, $sections[2]['section']);
1198         $this->assertEquals(3, $sections[3]['section']);
1199         // The module with the availability restriction met is returning contents.
1200         $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
1201         // The module with the availability restriction not met is not returning contents.
1202         $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
1204         // Now include flag for returning stealth information (fake section).
1205         $sections = core_course_external::get_course_contents($course->id,
1206             array(array("name" => "includestealthmodules", "value" => 1)));
1207         // We need to execute the return values cleaning process to simulate the web service server.
1208         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1210         $this->assertCount(5, $sections); // Include fake section with stealth activities.
1211         $this->assertCount(5, $sections[0]['modules']);
1212         $this->assertCount(1, $sections[1]['modules']);
1213         $this->assertCount(1, $sections[2]['modules']);
1214         $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1215         $this->assertCount(1, $sections[4]['modules']); // One stealth module.
1216         $this->assertEquals(-1, $sections[4]['id']);
1217     }
1219     /**
1220      * Test get_course_contents excluding modules
1221      */
1222     public function test_get_course_contents_excluding_modules() {
1223         $this->resetAfterTest(true);
1225         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1227         // Test exclude modules.
1228         $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludemodules", "value" => 1)));
1230         // We need to execute the return values cleaning process to simulate the web service server.
1231         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1233         $this->assertEmpty($sections[0]['modules']);
1234         $this->assertEmpty($sections[1]['modules']);
1235     }
1237     /**
1238      * Test get_course_contents excluding contents
1239      */
1240     public function test_get_course_contents_excluding_contents() {
1241         $this->resetAfterTest(true);
1243         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1245         // Test exclude modules.
1246         $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludecontents", "value" => 1)));
1248         // We need to execute the return values cleaning process to simulate the web service server.
1249         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1251         foreach ($sections as $section) {
1252             foreach ($section['modules'] as $module) {
1253                 // Only resources return contents.
1254                 if (isset($module['contents'])) {
1255                     $this->assertEmpty($module['contents']);
1256                 }
1257             }
1258         }
1259     }
1261     /**
1262      * Test get_course_contents filtering by section number
1263      */
1264     public function test_get_course_contents_section_number() {
1265         $this->resetAfterTest(true);
1267         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1269         // Test exclude modules.
1270         $sections = core_course_external::get_course_contents($course->id, array(array("name" => "sectionnumber", "value" => 0)));
1272         // We need to execute the return values cleaning process to simulate the web service server.
1273         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1275         $this->assertCount(1, $sections);
1276         $this->assertCount(5, $sections[0]['modules']);
1277     }
1279     /**
1280      * Test get_course_contents filtering by cmid
1281      */
1282     public function test_get_course_contents_cmid() {
1283         $this->resetAfterTest(true);
1285         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1287         // Test exclude modules.
1288         $sections = core_course_external::get_course_contents($course->id, array(array("name" => "cmid", "value" => $forumcm->id)));
1290         // We need to execute the return values cleaning process to simulate the web service server.
1291         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1293         $this->assertCount(4, $sections);
1294         $this->assertCount(1, $sections[0]['modules']);
1295         $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1296     }
1299     /**
1300      * Test get_course_contents filtering by cmid and section
1301      */
1302     public function test_get_course_contents_section_cmid() {
1303         $this->resetAfterTest(true);
1305         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1307         // Test exclude modules.
1308         $sections = core_course_external::get_course_contents($course->id, array(
1309                                                                         array("name" => "cmid", "value" => $forumcm->id),
1310                                                                         array("name" => "sectionnumber", "value" => 0)
1311                                                                         ));
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(1, $sections);
1317         $this->assertCount(1, $sections[0]['modules']);
1318         $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1319     }
1321     /**
1322      * Test get_course_contents filtering by modname
1323      */
1324     public function test_get_course_contents_modname() {
1325         $this->resetAfterTest(true);
1327         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1329         // Test exclude modules.
1330         $sections = core_course_external::get_course_contents($course->id, array(array("name" => "modname", "value" => "forum")));
1332         // We need to execute the return values cleaning process to simulate the web service server.
1333         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1335         $this->assertCount(4, $sections);
1336         $this->assertCount(1, $sections[0]['modules']);
1337         $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1338     }
1340     /**
1341      * Test get_course_contents filtering by modname
1342      */
1343     public function test_get_course_contents_modid() {
1344         $this->resetAfterTest(true);
1346         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1348         // Test exclude modules.
1349         $sections = core_course_external::get_course_contents($course->id, array(
1350                                                                             array("name" => "modname", "value" => "page"),
1351                                                                             array("name" => "modid", "value" => $pagecm->instance),
1352                                                                             ));
1354         // We need to execute the return values cleaning process to simulate the web service server.
1355         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1357         $this->assertCount(4, $sections);
1358         $this->assertCount(1, $sections[0]['modules']);
1359         $this->assertEquals("page", $sections[0]['modules'][0]["modname"]);
1360         $this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]);
1361     }
1363     /**
1364      * Test get course contents completion
1365      */
1366     public function test_get_course_contents_completion() {
1367         global $CFG;
1368         $this->resetAfterTest(true);
1370         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1371         availability_completion\condition::wipe_static_cache();
1373         // Test activity not completed yet.
1374         $result = core_course_external::get_course_contents($course->id, array(
1375             array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1376         // We need to execute the return values cleaning process to simulate the web service server.
1377         $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1379         $this->assertCount(1, $result[0]['modules']);
1380         $this->assertEquals("forum", $result[0]['modules'][0]["modname"]);
1381         $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1382         $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['state']);
1383         $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['timecompleted']);
1384         $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']);
1385         $this->assertFalse($result[0]['modules'][0]["completiondata"]['valueused']);
1387         // Set activity completed.
1388         core_completion_external::update_activity_completion_status_manually($forumcm->id, true);
1390         $result = core_course_external::get_course_contents($course->id, array(
1391             array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1392         // We need to execute the return values cleaning process to simulate the web service server.
1393         $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1395         $this->assertEquals(COMPLETION_COMPLETE, $result[0]['modules'][0]["completiondata"]['state']);
1396         $this->assertNotEmpty($result[0]['modules'][0]["completiondata"]['timecompleted']);
1397         $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']);
1399         // Test activity with completion value that is used in an availability condition.
1400         $result = core_course_external::get_course_contents($course->id, array(
1401                 array("name" => "modname", "value" => "label"), array("name" => "modid", "value" => $labelcm->instance)));
1402         // We need to execute the return values cleaning process to simulate the web service server.
1403         $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1405         $this->assertCount(1, $result[0]['modules']);
1406         $this->assertEquals("label", $result[0]['modules'][0]["modname"]);
1407         $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1408         $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['state']);
1409         $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['timecompleted']);
1410         $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']);
1411         $this->assertTrue($result[0]['modules'][0]["completiondata"]['valueused']);
1413         // Disable completion.
1414         $CFG->enablecompletion = 0;
1415         $result = core_course_external::get_course_contents($course->id, array(
1416             array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1417         // We need to execute the return values cleaning process to simulate the web service server.
1418         $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1420         $this->assertArrayNotHasKey('completiondata', $result[0]['modules'][0]);
1421     }
1423     /**
1424      * Test mimetype is returned for resources with showtype set.
1425      */
1426     public function test_get_course_contents_including_mimetype() {
1427         $this->resetAfterTest(true);
1429         $this->setAdminUser();
1430         $course = self::getDataGenerator()->create_course();
1432         $record = new stdClass();
1433         $record->course = $course->id;
1434         $record->showtype = 1;
1435         $resource = self::getDataGenerator()->create_module('resource', $record);
1437         $result = core_course_external::get_course_contents($course->id);
1438         $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1439         $this->assertCount(1, $result[0]['modules']);   // One module, first section.
1440         $customdata = unserialize(json_decode($result[0]['modules'][0]['customdata']));
1441         $this->assertEquals('text/plain', $customdata['filedetails']['mimetype']);
1442     }
1444     /**
1445      * Test contents info is returned.
1446      */
1447     public function test_get_course_contents_contentsinfo() {
1448         global $USER;
1450         $this->resetAfterTest(true);
1451         $this->setAdminUser();
1452         $timenow = time();
1454         $course = self::getDataGenerator()->create_course();
1456         $record = new stdClass();
1457         $record->course = $course->id;
1458         // One resource with one file.
1459         $resource1 = self::getDataGenerator()->create_module('resource', $record);
1461         // More type of files.
1462         $record->files = file_get_unused_draft_itemid();
1463         $usercontext = context_user::instance($USER->id);
1464         $extensions = array('txt', 'png', 'pdf');
1465         $fs = get_file_storage();
1466         foreach ($extensions as $key => $extension) {
1467             // Add actual file there.
1468             $filerecord = array('component' => 'user', 'filearea' => 'draft',
1469                     'contextid' => $usercontext->id, 'itemid' => $record->files,
1470                     'filename' => 'resource' . $key . '.' . $extension, 'filepath' => '/');
1471             $fs->create_file_from_string($filerecord, 'Test resource ' . $key . ' file');
1472         }
1474         // Create file reference.
1475         $repos = repository::get_instances(array('type' => 'user'));
1476         $userrepository = reset($repos);
1478         // Create a user private file.
1479         $userfilerecord = new stdClass;
1480         $userfilerecord->contextid = $usercontext->id;
1481         $userfilerecord->component = 'user';
1482         $userfilerecord->filearea  = 'private';
1483         $userfilerecord->itemid    = 0;
1484         $userfilerecord->filepath  = '/';
1485         $userfilerecord->filename  = 'userfile.txt';
1486         $userfilerecord->source    = 'test';
1487         $userfile = $fs->create_file_from_string($userfilerecord, 'User file content');
1488         $userfileref = $fs->pack_reference($userfilerecord);
1490         // Clone latest "normal" file.
1491         $filerefrecord = clone (object) $filerecord;
1492         $filerefrecord->filename = 'testref.txt';
1493         $fileref = $fs->create_file_from_reference($filerefrecord, $userrepository->id, $userfileref);
1494         // Set main file pointing to the file reference.
1495         file_set_sortorder($usercontext->id, 'user', 'draft', $record->files, $filerefrecord->filepath,
1496             $filerefrecord->filename, 1);
1498         // Once the reference has been created, create the file resource.
1499         $resource2 = self::getDataGenerator()->create_module('resource', $record);
1501         $result = core_course_external::get_course_contents($course->id);
1502         $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1503         $this->assertCount(2, $result[0]['modules']);
1504         foreach ($result[0]['modules'] as $module) {
1505             if ($module['instance'] == $resource1->id) {
1506                 $this->assertEquals(1, $module['contentsinfo']['filescount']);
1507                 $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
1508                 $this->assertEquals($module['contents'][0]['filesize'], $module['contentsinfo']['filessize']);
1509                 $this->assertEquals(array('text/plain'), $module['contentsinfo']['mimetypes']);
1510             } else {
1511                 $this->assertEquals(count($extensions) + 1, $module['contentsinfo']['filescount']);
1512                 $filessize = $module['contents'][0]['filesize'] + $module['contents'][1]['filesize'] +
1513                     $module['contents'][2]['filesize'] + $module['contents'][3]['filesize'];
1514                 $this->assertEquals($filessize, $module['contentsinfo']['filessize']);
1515                 $this->assertEquals('user', $module['contentsinfo']['repositorytype']);
1516                 $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
1517                 $this->assertEquals(array('text/plain', 'image/png', 'application/pdf'), $module['contentsinfo']['mimetypes']);
1518             }
1519         }
1520     }
1522     /**
1523      * Test get_course_contents when hidden sections are displayed.
1524      */
1525     public function test_get_course_contents_hiddensections() {
1526         global $DB;
1527         $this->resetAfterTest(true);
1529         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1530         // Force returning hidden sections.
1531         $course->hiddensections = 0;
1532         update_course($course);
1534         $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
1535         $user = self::getDataGenerator()->create_user();
1536         self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
1537         $this->setUser($user);
1539         $sections = core_course_external::get_course_contents($course->id, array());
1540         // We need to execute the return values cleaning process to simulate the web service server.
1541         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1543         $this->assertCount(5, $sections); // All the sections, including the "not visible" one.
1544         $this->assertCount(5, $sections[0]['modules']);
1545         $this->assertCount(1, $sections[1]['modules']);
1546         $this->assertCount(1, $sections[2]['modules']);
1547         $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1548         $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
1550         $this->assertNotEmpty($sections[3]['availabilityinfo']);
1551         $this->assertEquals(1, $sections[1]['section']);
1552         $this->assertEquals(2, $sections[2]['section']);
1553         $this->assertEquals(3, $sections[3]['section']);
1554         // The module with the availability restriction met is returning contents.
1555         $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
1556         // The module with the availability restriction not met is not returning contents.
1557         $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
1559         // Now include flag for returning stealth information (fake section).
1560         $sections = core_course_external::get_course_contents($course->id,
1561             array(array("name" => "includestealthmodules", "value" => 1)));
1562         // We need to execute the return values cleaning process to simulate the web service server.
1563         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1565         $this->assertCount(6, $sections); // Include fake section with stealth activities.
1566         $this->assertCount(5, $sections[0]['modules']);
1567         $this->assertCount(1, $sections[1]['modules']);
1568         $this->assertCount(1, $sections[2]['modules']);
1569         $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1570         $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
1571         $this->assertCount(1, $sections[5]['modules']); // One stealth module.
1572         $this->assertEquals(-1, $sections[5]['id']);
1573     }
1575     /**
1576      * Test duplicate_course
1577      */
1578     public function test_duplicate_course() {
1579         $this->resetAfterTest(true);
1581         // Create one course with three modules.
1582         $course  = self::getDataGenerator()->create_course();
1583         $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
1584         $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1585         $forumcontext = context_module::instance($forum->cmid);
1586         $data = $this->getDataGenerator()->create_module('data', array('assessed'=>1, 'scale'=>100, 'course'=>$course->id));
1587         $datacontext = context_module::instance($data->cmid);
1588         $datacm = get_coursemodule_from_instance('page', $data->id);
1589         $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
1590         $pagecontext = context_module::instance($page->cmid);
1591         $pagecm = get_coursemodule_from_instance('page', $page->id);
1593         // Set the required capabilities by the external function.
1594         $coursecontext = context_course::instance($course->id);
1595         $categorycontext = context_coursecat::instance($course->category);
1596         $roleid = $this->assignUserCapability('moodle/course:create', $categorycontext->id);
1597         $this->assignUserCapability('moodle/course:view', $categorycontext->id, $roleid);
1598         $this->assignUserCapability('moodle/restore:restorecourse', $categorycontext->id, $roleid);
1599         $this->assignUserCapability('moodle/backup:backupcourse', $coursecontext->id, $roleid);
1600         $this->assignUserCapability('moodle/backup:configure', $coursecontext->id, $roleid);
1601         // Optional capabilities to copy user data.
1602         $this->assignUserCapability('moodle/backup:userinfo', $coursecontext->id, $roleid);
1603         $this->assignUserCapability('moodle/restore:userinfo', $categorycontext->id, $roleid);
1605         $newcourse['fullname'] = 'Course duplicate';
1606         $newcourse['shortname'] = 'courseduplicate';
1607         $newcourse['categoryid'] = $course->category;
1608         $newcourse['visible'] = true;
1609         $newcourse['options'][] = array('name' => 'users', 'value' => true);
1611         $duplicate = core_course_external::duplicate_course($course->id, $newcourse['fullname'],
1612                 $newcourse['shortname'], $newcourse['categoryid'], $newcourse['visible'], $newcourse['options']);
1614         // We need to execute the return values cleaning process to simulate the web service server.
1615         $duplicate = external_api::clean_returnvalue(core_course_external::duplicate_course_returns(), $duplicate);
1617         // Check that the course has been duplicated.
1618         $this->assertEquals($newcourse['shortname'], $duplicate['shortname']);
1619     }
1621     /**
1622      * Test update_courses
1623      */
1624     public function test_update_courses() {
1625         global $DB, $CFG, $USER, $COURSE;
1627         // Get current $COURSE to be able to restore it later (defaults to $SITE). We need this
1628         // trick because we are both updating and getting (for testing) course information
1629         // in the same request and core_course_external::update_courses()
1630         // is overwriting $COURSE all over the time with OLD values, so later
1631         // use of get_course() fetches those OLD values instead of the updated ones.
1632         // See MDL-39723 for more info.
1633         $origcourse = clone($COURSE);
1635         $this->resetAfterTest(true);
1637         // Set the required capabilities by the external function.
1638         $contextid = context_system::instance()->id;
1639         $roleid = $this->assignUserCapability('moodle/course:update', $contextid);
1640         $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1641         $this->assignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
1642         $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1643         $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
1644         $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
1645         $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
1646         $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
1647         $this->assignUserCapability('moodle/course:viewhiddencourses', $contextid, $roleid);
1648         $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
1650         // Create category and courses.
1651         $category1  = self::getDataGenerator()->create_category();
1652         $category2  = self::getDataGenerator()->create_category();
1654         $originalcourse1 = self::getDataGenerator()->create_course();
1655         self::getDataGenerator()->enrol_user($USER->id, $originalcourse1->id, $roleid);
1657         $originalcourse2 = self::getDataGenerator()->create_course();
1658         self::getDataGenerator()->enrol_user($USER->id, $originalcourse2->id, $roleid);
1660         // Course with custom fields.
1661         $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
1662         $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
1663             'categoryid' => $fieldcategory->get('id'),
1664             'configdata' => ['visibility' => \core_course\customfield\course_handler::VISIBLETOALL, 'locked' => 1]];
1665         $field = self::getDataGenerator()->create_custom_field($customfield);
1667         $originalcourse3 = self::getDataGenerator()->create_course(['customfield_test' => 'Test value']);
1668         self::getDataGenerator()->enrol_user($USER->id, $originalcourse3->id, $roleid);
1670         // Course values to be updated.
1671         $course1['id'] = $originalcourse1->id;
1672         $course1['fullname'] = 'Updated test course 1';
1673         $course1['shortname'] = 'Udestedtestcourse1';
1674         $course1['categoryid'] = $category1->id;
1676         $course2['id'] = $originalcourse2->id;
1677         $course2['fullname'] = 'Updated test course 2';
1678         $course2['shortname'] = 'Updestedtestcourse2';
1679         $course2['categoryid'] = $category2->id;
1680         $course2['idnumber'] = 'Updatedidnumber2';
1681         $course2['summary'] = 'Updaated description for course 2';
1682         $course2['summaryformat'] = FORMAT_HTML;
1683         $course2['format'] = 'topics';
1684         $course2['showgrades'] = 1;
1685         $course2['newsitems'] = 3;
1686         $course2['startdate'] = 1420092000; // 01/01/2015.
1687         $course2['enddate'] = 1422669600; // 01/31/2015.
1688         $course2['maxbytes'] = 100000;
1689         $course2['showreports'] = 1;
1690         $course2['visible'] = 0;
1691         $course2['hiddensections'] = 0;
1692         $course2['groupmode'] = 0;
1693         $course2['groupmodeforce'] = 0;
1694         $course2['defaultgroupingid'] = 0;
1695         $course2['enablecompletion'] = 1;
1696         $course2['lang'] = 'en';
1697         $course2['forcetheme'] = 'classic';
1699         $course3['id'] = $originalcourse3->id;
1700         $updatedcustomfieldvalue = ['shortname' => 'test', 'value' => 'Updated test value'];
1701         $course3['customfields'] = [$updatedcustomfieldvalue];
1702         $courses = array($course1, $course2, $course3);
1704         $updatedcoursewarnings = core_course_external::update_courses($courses);
1705         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1706                 $updatedcoursewarnings);
1707         $COURSE = $origcourse; // Restore $COURSE. Instead of using the OLD one set by the previous line.
1709         // Check that right number of courses were created.
1710         $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1712         // Check that the courses were correctly created.
1713         foreach ($courses as $course) {
1714             $courseinfo = course_get_format($course['id'])->get_course();
1715             $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course['id']);
1716             if ($course['id'] == $course2['id']) {
1717                 $this->assertEquals($course2['fullname'], $courseinfo->fullname);
1718                 $this->assertEquals($course2['shortname'], $courseinfo->shortname);
1719                 $this->assertEquals($course2['categoryid'], $courseinfo->category);
1720                 $this->assertEquals($course2['idnumber'], $courseinfo->idnumber);
1721                 $this->assertEquals($course2['summary'], $courseinfo->summary);
1722                 $this->assertEquals($course2['summaryformat'], $courseinfo->summaryformat);
1723                 $this->assertEquals($course2['format'], $courseinfo->format);
1724                 $this->assertEquals($course2['showgrades'], $courseinfo->showgrades);
1725                 $this->assertEquals($course2['newsitems'], $courseinfo->newsitems);
1726                 $this->assertEquals($course2['startdate'], $courseinfo->startdate);
1727                 $this->assertEquals($course2['enddate'], $courseinfo->enddate);
1728                 $this->assertEquals($course2['maxbytes'], $courseinfo->maxbytes);
1729                 $this->assertEquals($course2['showreports'], $courseinfo->showreports);
1730                 $this->assertEquals($course2['visible'], $courseinfo->visible);
1731                 $this->assertEquals($course2['hiddensections'], $courseinfo->hiddensections);
1732                 $this->assertEquals($course2['groupmode'], $courseinfo->groupmode);
1733                 $this->assertEquals($course2['groupmodeforce'], $courseinfo->groupmodeforce);
1734                 $this->assertEquals($course2['defaultgroupingid'], $courseinfo->defaultgroupingid);
1735                 $this->assertEquals($course2['lang'], $courseinfo->lang);
1737                 if (!empty($CFG->allowcoursethemes)) {
1738                     $this->assertEquals($course2['forcetheme'], $courseinfo->theme);
1739                 }
1741                 $this->assertEquals($course2['enablecompletion'], $courseinfo->enablecompletion);
1742                 $this->assertEquals(['test' => null], (array)$customfields);
1743             } else if ($course['id'] == $course1['id']) {
1744                 $this->assertEquals($course1['fullname'], $courseinfo->fullname);
1745                 $this->assertEquals($course1['shortname'], $courseinfo->shortname);
1746                 $this->assertEquals($course1['categoryid'], $courseinfo->category);
1747                 $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
1748                 $this->assertEquals('topics', $courseinfo->format);
1749                 $this->assertEquals(5, course_get_format($course['id'])->get_last_section_number());
1750                 $this->assertEquals(0, $courseinfo->newsitems);
1751                 $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
1752                 $this->assertEquals(['test' => null], (array)$customfields);
1753             } else if ($course['id'] == $course3['id']) {
1754                 $this->assertEquals(['test' => $updatedcustomfieldvalue['value']], (array)$customfields);
1755             } else {
1756                 throw new moodle_exception('Unexpected shortname');
1757             }
1758         }
1760         $courses = array($course1);
1761         // Try update course without update capability.
1762         $user = self::getDataGenerator()->create_user();
1763         $this->setUser($user);
1764         $this->unassignUserCapability('moodle/course:update', $contextid, $roleid);
1765         self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1766         $updatedcoursewarnings = core_course_external::update_courses($courses);
1767         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1768                                                                     $updatedcoursewarnings);
1769         $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1771         // Try update course category without capability.
1772         $this->assignUserCapability('moodle/course:update', $contextid, $roleid);
1773         $this->unassignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1774         $user = self::getDataGenerator()->create_user();
1775         $this->setUser($user);
1776         self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1777         $course1['categoryid'] = $category2->id;
1778         $courses = array($course1);
1779         $updatedcoursewarnings = core_course_external::update_courses($courses);
1780         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1781                                                                     $updatedcoursewarnings);
1782         $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1784         // Try update course fullname without capability.
1785         $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1786         $this->unassignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1787         $user = self::getDataGenerator()->create_user();
1788         $this->setUser($user);
1789         self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1790         $updatedcoursewarnings = core_course_external::update_courses($courses);
1791         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1792                                                                     $updatedcoursewarnings);
1793         $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1794         $course1['fullname'] = 'Testing fullname without permission';
1795         $courses = array($course1);
1796         $updatedcoursewarnings = core_course_external::update_courses($courses);
1797         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1798                                                                     $updatedcoursewarnings);
1799         $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1801         // Try update course shortname without capability.
1802         $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1803         $this->unassignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
1804         $user = self::getDataGenerator()->create_user();
1805         $this->setUser($user);
1806         self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1807         $updatedcoursewarnings = core_course_external::update_courses($courses);
1808         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1809                                                                     $updatedcoursewarnings);
1810         $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1811         $course1['shortname'] = 'Testing shortname without permission';
1812         $courses = array($course1);
1813         $updatedcoursewarnings = core_course_external::update_courses($courses);
1814         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1815                                                                     $updatedcoursewarnings);
1816         $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1818         // Try update course idnumber without capability.
1819         $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
1820         $this->unassignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
1821         $user = self::getDataGenerator()->create_user();
1822         $this->setUser($user);
1823         self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1824         $updatedcoursewarnings = core_course_external::update_courses($courses);
1825         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1826                                                                     $updatedcoursewarnings);
1827         $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1828         $course1['idnumber'] = 'NEWIDNUMBER';
1829         $courses = array($course1);
1830         $updatedcoursewarnings = core_course_external::update_courses($courses);
1831         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1832                                                                     $updatedcoursewarnings);
1833         $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1835         // Try update course summary without capability.
1836         $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
1837         $this->unassignUserCapability('moodle/course:changesummary', $contextid, $roleid);
1838         $user = self::getDataGenerator()->create_user();
1839         $this->setUser($user);
1840         self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1841         $updatedcoursewarnings = core_course_external::update_courses($courses);
1842         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1843                                                                     $updatedcoursewarnings);
1844         $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1845         $course1['summary'] = 'New summary';
1846         $courses = array($course1);
1847         $updatedcoursewarnings = core_course_external::update_courses($courses);
1848         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1849                                                                     $updatedcoursewarnings);
1850         $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1852         // Try update course with invalid summary format.
1853         $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
1854         $user = self::getDataGenerator()->create_user();
1855         $this->setUser($user);
1856         self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1857         $updatedcoursewarnings = core_course_external::update_courses($courses);
1858         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1859                                                                     $updatedcoursewarnings);
1860         $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1861         $course1['summaryformat'] = 10;
1862         $courses = array($course1);
1863         $updatedcoursewarnings = core_course_external::update_courses($courses);
1864         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1865                                                                     $updatedcoursewarnings);
1866         $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1868         // Try update course visibility without capability.
1869         $this->unassignUserCapability('moodle/course:visibility', $contextid, $roleid);
1870         $user = self::getDataGenerator()->create_user();
1871         $this->setUser($user);
1872         self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1873         $course1['summaryformat'] = FORMAT_MOODLE;
1874         $courses = array($course1);
1875         $updatedcoursewarnings = core_course_external::update_courses($courses);
1876         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1877                                                                     $updatedcoursewarnings);
1878         $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1879         $course1['visible'] = 0;
1880         $courses = array($course1);
1881         $updatedcoursewarnings = core_course_external::update_courses($courses);
1882         $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1883                                                                     $updatedcoursewarnings);
1884         $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1886         // Try update course custom fields without capability.
1887         $this->unassignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
1888         $user = self::getDataGenerator()->create_user();
1889         $this->setUser($user);
1890         self::getDataGenerator()->enrol_user($user->id, $course3['id'], $roleid);
1892         $newupdatedcustomfieldvalue = ['shortname' => 'test', 'value' => 'New updated value'];
1893         $course3['customfields'] = [$newupdatedcustomfieldvalue];
1895         core_course_external::update_courses([$course3]);
1897         // Custom field was not updated.
1898         $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course3['id']);
1899         $this->assertEquals(['test' => $updatedcustomfieldvalue['value']], (array)$customfields);
1900     }
1902     /**
1903      * Test delete course_module.
1904      */
1905     public function test_delete_modules() {
1906         global $DB;
1908         // Ensure we reset the data after this test.
1909         $this->resetAfterTest(true);
1911         // Create a user.
1912         $user = self::getDataGenerator()->create_user();
1914         // Set the tests to run as the user.
1915         self::setUser($user);
1917         // Create a course to add the modules.
1918         $course = self::getDataGenerator()->create_course();
1920         // Create two test modules.
1921         $record = new stdClass();
1922         $record->course = $course->id;
1923         $module1 = self::getDataGenerator()->create_module('forum', $record);
1924         $module2 = self::getDataGenerator()->create_module('assign', $record);
1926         // Check the forum was correctly created.
1927         $this->assertEquals(1, $DB->count_records('forum', array('id' => $module1->id)));
1929         // Check the assignment was correctly created.
1930         $this->assertEquals(1, $DB->count_records('assign', array('id' => $module2->id)));
1932         // Check data exists in the course modules table.
1933         $this->assertEquals(2, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
1934                 array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
1936         // Enrol the user in the course.
1937         $enrol = enrol_get_plugin('manual');
1938         $enrolinstances = enrol_get_instances($course->id, true);
1939         foreach ($enrolinstances as $courseenrolinstance) {
1940             if ($courseenrolinstance->enrol == "manual") {
1941                 $instance = $courseenrolinstance;
1942                 break;
1943             }
1944         }
1945         $enrol->enrol_user($instance, $user->id);
1947         // Assign capabilities to delete module 1.
1948         $modcontext = context_module::instance($module1->cmid);
1949         $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id);
1951         // Assign capabilities to delete module 2.
1952         $modcontext = context_module::instance($module2->cmid);
1953         $newrole = create_role('Role 2', 'role2', 'Role 2 description');
1954         $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id, $newrole);
1956         // Deleting these module instances.
1957         core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
1959         // Check the forum was deleted.
1960         $this->assertEquals(0, $DB->count_records('forum', array('id' => $module1->id)));
1962         // Check the assignment was deleted.
1963         $this->assertEquals(0, $DB->count_records('assign', array('id' => $module2->id)));
1965         // Check we retrieve no data in the course modules table.
1966         $this->assertEquals(0, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
1967                 array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
1969         // Call with non-existent course module id and ensure exception thrown.
1970         try {
1971             core_course_external::delete_modules(array('1337'));
1972             $this->fail('Exception expected due to missing course module.');
1973         } catch (dml_missing_record_exception $e) {
1974             $this->assertEquals('invalidcoursemodule', $e->errorcode);
1975         }
1977         // Create two modules.
1978         $module1 = self::getDataGenerator()->create_module('forum', $record);
1979         $module2 = self::getDataGenerator()->create_module('assign', $record);
1981         // Since these modules were recreated the user will not have capabilities
1982         // to delete them, ensure exception is thrown if they try.
1983         try {
1984             core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
1985             $this->fail('Exception expected due to missing capability.');
1986         } catch (moodle_exception $e) {
1987             $this->assertEquals('nopermissions', $e->errorcode);
1988         }
1990         // Unenrol user from the course.
1991         $enrol->unenrol_user($instance, $user->id);
1993         // Try and delete modules from the course the user was unenrolled in, make sure exception thrown.
1994         try {
1995             core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
1996             $this->fail('Exception expected due to being unenrolled from the course.');
1997         } catch (moodle_exception $e) {
1998             $this->assertEquals('requireloginerror', $e->errorcode);
1999         }
2000     }
2002     /**
2003      * Test import_course into an empty course
2004      */
2005     public function test_import_course_empty() {
2006         global $USER;
2008         $this->resetAfterTest(true);
2010         $course1  = self::getDataGenerator()->create_course();
2011         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id, 'name' => 'Forum test'));
2012         $page = $this->getDataGenerator()->create_module('page', array('course' => $course1->id, 'name' => 'Page test'));
2014         $course2  = self::getDataGenerator()->create_course();
2016         $course1cms = get_fast_modinfo($course1->id)->get_cms();
2017         $course2cms = get_fast_modinfo($course2->id)->get_cms();
2019         // Verify the state of the courses before we do the import.
2020         $this->assertCount(2, $course1cms);
2021         $this->assertEmpty($course2cms);
2023         // Setup the user to run the operation (ugly hack because validate_context() will
2024         // fail as the email is not set by $this->setAdminUser()).
2025         $this->setAdminUser();
2026         $USER->email = 'emailtopass@example.com';
2028         // Import from course1 to course2.
2029         core_course_external::import_course($course1->id, $course2->id, 0);
2031         // Verify that now we have two modules in both courses.
2032         $course1cms = get_fast_modinfo($course1->id)->get_cms();
2033         $course2cms = get_fast_modinfo($course2->id)->get_cms();
2034         $this->assertCount(2, $course1cms);
2035         $this->assertCount(2, $course2cms);
2037         // Verify that the names transfered across correctly.
2038         foreach ($course2cms as $cm) {
2039             if ($cm->modname === 'page') {
2040                 $this->assertEquals($cm->name, $page->name);
2041             } else if ($cm->modname === 'forum') {
2042                 $this->assertEquals($cm->name, $forum->name);
2043             } else {
2044                 $this->fail('Unknown CM found.');
2045             }
2046         }
2047     }
2049     /**
2050      * Test import_course into an filled course
2051      */
2052     public function test_import_course_filled() {
2053         global $USER;
2055         $this->resetAfterTest(true);
2057         // Add forum and page to course1.
2058         $course1  = self::getDataGenerator()->create_course();
2059         $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2060         $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
2062         // Add quiz to course 2.
2063         $course2  = self::getDataGenerator()->create_course();
2064         $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
2066         $course1cms = get_fast_modinfo($course1->id)->get_cms();
2067         $course2cms = get_fast_modinfo($course2->id)->get_cms();
2069         // Verify the state of the courses before we do the import.
2070         $this->assertCount(2, $course1cms);
2071         $this->assertCount(1, $course2cms);
2073         // Setup the user to run the operation (ugly hack because validate_context() will
2074         // fail as the email is not set by $this->setAdminUser()).
2075         $this->setAdminUser();
2076         $USER->email = 'emailtopass@example.com';
2078         // Import from course1 to course2 without deleting content.
2079         core_course_external::import_course($course1->id, $course2->id, 0);
2081         $course2cms = get_fast_modinfo($course2->id)->get_cms();
2083         // Verify that now we have three modules in course2.
2084         $this->assertCount(3, $course2cms);
2086         // Verify that the names transfered across correctly.
2087         foreach ($course2cms as $cm) {
2088             if ($cm->modname === 'page') {
2089                 $this->assertEquals($cm->name, $page->name);
2090             } else if ($cm->modname === 'forum') {
2091                 $this->assertEquals($cm->name, $forum->name);
2092             } else if ($cm->modname === 'quiz') {
2093                 $this->assertEquals($cm->name, $quiz->name);
2094             } else {
2095                 $this->fail('Unknown CM found.');
2096             }
2097         }
2098     }
2100     /**
2101      * Test import_course with only blocks set to backup
2102      */
2103     public function test_import_course_blocksonly() {
2104         global $USER, $DB;
2106         $this->resetAfterTest(true);
2108         // Add forum and page to course1.
2109         $course1  = self::getDataGenerator()->create_course();
2110         $course1ctx = context_course::instance($course1->id);
2111         $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2112         $block = $this->getDataGenerator()->create_block('online_users', array('parentcontextid' => $course1ctx->id));
2114         $course2  = self::getDataGenerator()->create_course();
2115         $course2ctx = context_course::instance($course2->id);
2116         $initialblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
2117         $initialcmcount = count(get_fast_modinfo($course2->id)->get_cms());
2119         // Setup the user to run the operation (ugly hack because validate_context() will
2120         // fail as the email is not set by $this->setAdminUser()).
2121         $this->setAdminUser();
2122         $USER->email = 'emailtopass@example.com';
2124         // Import from course1 to course2 without deleting content, but excluding
2125         // activities.
2126         $options = array(
2127             array('name' => 'activities', 'value' => 0),
2128             array('name' => 'blocks', 'value' => 1),
2129             array('name' => 'filters', 'value' => 0),
2130         );
2132         core_course_external::import_course($course1->id, $course2->id, 0, $options);
2134         $newcmcount = count(get_fast_modinfo($course2->id)->get_cms());
2135         $newblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
2136         // Check that course modules haven't changed, but that blocks have.
2137         $this->assertEquals($initialcmcount, $newcmcount);
2138         $this->assertEquals(($initialblockcount + 1), $newblockcount);
2139     }
2141     /**
2142      * Test import_course into an filled course, deleting content.
2143      */
2144     public function test_import_course_deletecontent() {
2145         global $USER;
2146         $this->resetAfterTest(true);
2148         // Add forum and page to course1.
2149         $course1  = self::getDataGenerator()->create_course();
2150         $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2151         $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
2153         // Add quiz to course 2.
2154         $course2  = self::getDataGenerator()->create_course();
2155         $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
2157         $course1cms = get_fast_modinfo($course1->id)->get_cms();
2158         $course2cms = get_fast_modinfo($course2->id)->get_cms();
2160         // Verify the state of the courses before we do the import.
2161         $this->assertCount(2, $course1cms);
2162         $this->assertCount(1, $course2cms);
2164         // Setup the user to run the operation (ugly hack because validate_context() will
2165         // fail as the email is not set by $this->setAdminUser()).
2166         $this->setAdminUser();
2167         $USER->email = 'emailtopass@example.com';
2169         // Import from course1 to course2,  deleting content.
2170         core_course_external::import_course($course1->id, $course2->id, 1);
2172         $course2cms = get_fast_modinfo($course2->id)->get_cms();
2174         // Verify that now we have two modules in course2.
2175         $this->assertCount(2, $course2cms);
2177         // Verify that the course only contains the imported modules.
2178         foreach ($course2cms as $cm) {
2179             if ($cm->modname === 'page') {
2180                 $this->assertEquals($cm->name, $page->name);
2181             } else if ($cm->modname === 'forum') {
2182                 $this->assertEquals($cm->name, $forum->name);
2183             } else {
2184                 $this->fail('Unknown CM found: '.$cm->name);
2185             }
2186         }
2187     }
2189     /**
2190      * Ensure import_course handles incorrect deletecontent option correctly.
2191      */
2192     public function test_import_course_invalid_deletecontent_option() {
2193         $this->resetAfterTest(true);
2195         $course1  = self::getDataGenerator()->create_course();
2196         $course2  = self::getDataGenerator()->create_course();
2198         $this->expectException('moodle_exception');
2199         $this->expectExceptionMessage(get_string('invalidextparam', 'webservice', -1));
2200         // Import from course1 to course2, with invalid option
2201         core_course_external::import_course($course1->id, $course2->id, -1);;
2202     }
2204     /**
2205      * Test view_course function
2206      */
2207     public function test_view_course() {
2209         $this->resetAfterTest();
2211         // Course without sections.
2212         $course = $this->getDataGenerator()->create_course(array('numsections' => 5), array('createsections' => true));
2213         $this->setAdminUser();
2215         // Redirect events to the sink, so we can recover them later.
2216         $sink = $this->redirectEvents();
2218         $result = core_course_external::view_course($course->id, 1);
2219         $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
2220         $events = $sink->get_events();
2221         $event = reset($events);
2223         // Check the event details are correct.
2224         $this->assertInstanceOf('\core\event\course_viewed', $event);
2225         $this->assertEquals(context_course::instance($course->id), $event->get_context());
2226         $this->assertEquals(1, $event->other['coursesectionnumber']);
2228         $result = core_course_external::view_course($course->id);
2229         $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
2230         $events = $sink->get_events();
2231         $event = array_pop($events);
2232         $sink->close();
2234         // Check the event details are correct.
2235         $this->assertInstanceOf('\core\event\course_viewed', $event);
2236         $this->assertEquals(context_course::instance($course->id), $event->get_context());
2237         $this->assertEmpty($event->other);
2239     }
2241     /**
2242      * Test get_course_module
2243      */
2244     public function test_get_course_module() {
2245         global $DB;
2247         $this->resetAfterTest(true);
2249         $this->setAdminUser();
2250         $course = self::getDataGenerator()->create_course();
2251         $record = array(
2252             'course' => $course->id,
2253             'name' => 'First Assignment'
2254         );
2255         $options = array(
2256             'idnumber' => 'ABC',
2257             'visible' => 0
2258         );
2259         // Hidden activity.
2260         $assign = self::getDataGenerator()->create_module('assign', $record, $options);
2262         $outcomescale = 'Distinction, Very Good, Good, Pass, Fail';
2264         // Insert a custom grade scale to be used by an outcome.
2265         $gradescale = new grade_scale();
2266         $gradescale->name        = 'gettcoursemodulescale';
2267         $gradescale->courseid    = $course->id;
2268         $gradescale->userid      = 0;
2269         $gradescale->scale       = $outcomescale;
2270         $gradescale->description = 'This scale is used to mark standard assignments.';
2271         $gradescale->insert();
2273         // Insert an outcome.
2274         $data = new stdClass();
2275         $data->courseid = $course->id;
2276         $data->fullname = 'Team work';
2277         $data->shortname = 'Team work';
2278         $data->scaleid = $gradescale->id;
2279         $outcome = new grade_outcome($data, false);
2280         $outcome->insert();
2282         $outcomegradeitem = new grade_item();
2283         $outcomegradeitem->itemname = $outcome->shortname;
2284         $outcomegradeitem->itemtype = 'mod';
2285         $outcomegradeitem->itemmodule = 'assign';
2286         $outcomegradeitem->iteminstance = $assign->id;
2287         $outcomegradeitem->outcomeid = $outcome->id;
2288         $outcomegradeitem->cmid = 0;
2289         $outcomegradeitem->courseid = $course->id;
2290         $outcomegradeitem->aggregationcoef = 0;
2291         $outcomegradeitem->itemnumber = 1000; // Outcomes start at 1000.
2292         $outcomegradeitem->gradetype = GRADE_TYPE_SCALE;
2293         $outcomegradeitem->scaleid = $outcome->scaleid;
2294         $outcomegradeitem->insert();
2296         $assignmentgradeitem = grade_item::fetch(
2297             array(
2298                 'itemtype' => 'mod',
2299                 'itemmodule' => 'assign',
2300                 'iteminstance' => $assign->id,
2301                 'itemnumber' => 0,
2302                 'courseid' => $course->id
2303             )
2304         );
2305         $outcomegradeitem->set_parent($assignmentgradeitem->categoryid);
2306         $outcomegradeitem->move_after_sortorder($assignmentgradeitem->sortorder);
2308         // Test admin user can see the complete hidden activity.
2309         $result = core_course_external::get_course_module($assign->cmid);
2310         $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
2312         $this->assertCount(0, $result['warnings']);
2313         // Test we retrieve all the fields.
2314         $this->assertCount(28, $result['cm']);
2315         $this->assertEquals($record['name'], $result['cm']['name']);
2316         $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
2317         $this->assertEquals(100, $result['cm']['grade']);
2318         $this->assertEquals(0.0, $result['cm']['gradepass']);
2319         $this->assertEquals('submissions', $result['cm']['advancedgrading'][0]['area']);
2320         $this->assertEmpty($result['cm']['advancedgrading'][0]['method']);
2321         $this->assertEquals($outcomescale, $result['cm']['outcomes'][0]['scale']);
2323         $student = $this->getDataGenerator()->create_user();
2324         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2326         self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
2327         $this->setUser($student);
2329         // The user shouldn't be able to see the activity.
2330         try {
2331             core_course_external::get_course_module($assign->cmid);
2332             $this->fail('Exception expected due to invalid permissions.');
2333         } catch (moodle_exception $e) {
2334             $this->assertEquals('requireloginerror', $e->errorcode);
2335         }
2337         // Make module visible.
2338         set_coursemodule_visible($assign->cmid, 1);
2340         // Test student user.
2341         $result = core_course_external::get_course_module($assign->cmid);
2342         $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
2344         $this->assertCount(0, $result['warnings']);
2345         // Test we retrieve only the few files we can see.
2346         $this->assertCount(11, $result['cm']);
2347         $this->assertEquals($assign->cmid, $result['cm']['id']);
2348         $this->assertEquals($course->id, $result['cm']['course']);
2349         $this->assertEquals('assign', $result['cm']['modname']);
2350         $this->assertEquals($assign->id, $result['cm']['instance']);
2352     }
2354     /**
2355      * Test get_course_module_by_instance
2356      */
2357     public function test_get_course_module_by_instance() {
2358         global $DB;
2360         $this->resetAfterTest(true);
2362         $this->setAdminUser();
2363         $course = self::getDataGenerator()->create_course();
2364         $record = array(
2365             'course' => $course->id,
2366             'name' => 'First quiz',
2367             'grade' => 90.00
2368         );
2369         $options = array(
2370             'idnumber' => 'ABC',
2371             'visible' => 0
2372         );
2373         // Hidden activity.
2374         $quiz = self::getDataGenerator()->create_module('quiz', $record, $options);
2376         // Test admin user can see the complete hidden activity.
2377         $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2378         $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
2380         $this->assertCount(0, $result['warnings']);
2381         // Test we retrieve all the fields.
2382         $this->assertCount(26, $result['cm']);
2383         $this->assertEquals($record['name'], $result['cm']['name']);
2384         $this->assertEquals($record['grade'], $result['cm']['grade']);
2385         $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
2387         $student = $this->getDataGenerator()->create_user();
2388         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2390         self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
2391         $this->setUser($student);
2393         // The user shouldn't be able to see the activity.
2394         try {
2395             core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2396             $this->fail('Exception expected due to invalid permissions.');
2397         } catch (moodle_exception $e) {
2398             $this->assertEquals('requireloginerror', $e->errorcode);
2399         }
2401         // Make module visible.
2402         set_coursemodule_visible($quiz->cmid, 1);
2404         // Test student user.
2405         $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2406         $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
2408         $this->assertCount(0, $result['warnings']);
2409         // Test we retrieve only the few files we can see.
2410         $this->assertCount(11, $result['cm']);
2411         $this->assertEquals($quiz->cmid, $result['cm']['id']);
2412         $this->assertEquals($course->id, $result['cm']['course']);
2413         $this->assertEquals('quiz', $result['cm']['modname']);
2414         $this->assertEquals($quiz->id, $result['cm']['instance']);
2416         // Try with an invalid module name.
2417         try {
2418             core_course_external::get_course_module_by_instance('abc', $quiz->id);
2419             $this->fail('Exception expected due to invalid module name.');
2420         } catch (dml_read_exception $e) {
2421             $this->assertEquals('dmlreadexception', $e->errorcode);
2422         }
2424     }
2426     /**
2427      * Test get_user_navigation_options
2428      */
2429     public function test_get_user_navigation_options() {
2430         global $USER;
2432         $this->resetAfterTest();
2433         $course1 = self::getDataGenerator()->create_course();
2434         $course2 = self::getDataGenerator()->create_course();
2436         // Create a viewer user.
2437         $viewer = self::getDataGenerator()->create_user();
2438         $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2439         $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2441         $this->setUser($viewer->id);
2442         $courses = array($course1->id , $course2->id, SITEID);
2444         $result = core_course_external::get_user_navigation_options($courses);
2445         $result = external_api::clean_returnvalue(core_course_external::get_user_navigation_options_returns(), $result);
2447         $this->assertCount(0, $result['warnings']);
2448         $this->assertCount(3, $result['courses']);
2450         foreach ($result['courses'] as $course) {
2451             $navoptions = new stdClass;
2452             foreach ($course['options'] as $option) {
2453                 $navoptions->{$option['name']} = $option['available'];
2454             }
2455             $this->assertCount(9, $course['options']);
2456             if ($course['id'] == SITEID) {
2457                 $this->assertTrue($navoptions->blogs);
2458                 $this->assertFalse($navoptions->notes);
2459                 $this->assertFalse($navoptions->participants);
2460                 $this->assertTrue($navoptions->badges);
2461                 $this->assertTrue($navoptions->tags);
2462                 $this->assertFalse($navoptions->grades);
2463                 $this->assertFalse($navoptions->search);
2464                 $this->assertTrue($navoptions->calendar);
2465                 $this->assertTrue($navoptions->competencies);
2466             } else {
2467                 $this->assertTrue($navoptions->blogs);
2468                 $this->assertFalse($navoptions->notes);
2469                 $this->assertTrue($navoptions->participants);
2470                 $this->assertTrue($navoptions->badges);
2471                 $this->assertFalse($navoptions->tags);
2472                 $this->assertTrue($navoptions->grades);
2473                 $this->assertFalse($navoptions->search);
2474                 $this->assertFalse($navoptions->calendar);
2475                 $this->assertTrue($navoptions->competencies);
2476             }
2477         }
2478     }
2480     /**
2481      * Test get_user_administration_options
2482      */
2483     public function test_get_user_administration_options() {
2484         global $USER;
2486         $this->resetAfterTest();
2487         $course1 = self::getDataGenerator()->create_course();
2488         $course2 = self::getDataGenerator()->create_course();
2490         // Create a viewer user.
2491         $viewer = self::getDataGenerator()->create_user();
2492         $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2493         $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2495         $this->setUser($viewer->id);
2496         $courses = array($course1->id , $course2->id, SITEID);
2498         $result = core_course_external::get_user_administration_options($courses);
2499         $result = external_api::clean_returnvalue(core_course_external::get_user_administration_options_returns(), $result);
2501         $this->assertCount(0, $result['warnings']);
2502         $this->assertCount(3, $result['courses']);
2504         foreach ($result['courses'] as $course) {
2505             $adminoptions = new stdClass;
2506             foreach ($course['options'] as $option) {
2507                 $adminoptions->{$option['name']} = $option['available'];
2508             }
2509             if ($course['id'] == SITEID) {
2510                 $this->assertCount(17, $course['options']);
2511                 $this->assertFalse($adminoptions->update);
2512                 $this->assertFalse($adminoptions->filters);
2513                 $this->assertFalse($adminoptions->reports);
2514                 $this->assertFalse($adminoptions->backup);
2515                 $this->assertFalse($adminoptions->restore);
2516                 $this->assertFalse($adminoptions->files);
2517                 $this->assertFalse(!isset($adminoptions->tags));
2518                 $this->assertFalse($adminoptions->gradebook);
2519                 $this->assertFalse($adminoptions->outcomes);
2520                 $this->assertFalse($adminoptions->badges);
2521                 $this->assertFalse($adminoptions->import);
2522                 $this->assertFalse($adminoptions->reset);
2523                 $this->assertFalse($adminoptions->roles);
2524                 $this->assertFalse($adminoptions->editcompletion);
2525                 $this->assertFalse($adminoptions->copy);
2526             } else {
2527                 $this->assertCount(15, $course['options']);
2528                 $this->assertFalse($adminoptions->update);
2529                 $this->assertFalse($adminoptions->filters);
2530                 $this->assertFalse($adminoptions->reports);
2531                 $this->assertFalse($adminoptions->backup);
2532                 $this->assertFalse($adminoptions->restore);
2533                 $this->assertFalse($adminoptions->files);
2534                 $this->assertFalse($adminoptions->tags);
2535                 $this->assertFalse($adminoptions->gradebook);
2536                 $this->assertFalse($adminoptions->outcomes);
2537                 $this->assertTrue($adminoptions->badges);
2538                 $this->assertFalse($adminoptions->import);
2539                 $this->assertFalse($adminoptions->reset);
2540                 $this->assertFalse($adminoptions->roles);
2541                 $this->assertFalse($adminoptions->editcompletion);
2542                 $this->assertFalse($adminoptions->copy);
2543             }
2544         }
2545     }
2547     /**
2548      * Test get_courses_by_fields
2549      */
2550     public function test_get_courses_by_field() {
2551         global $DB;
2552         $this->resetAfterTest(true);
2554         $category1 = self::getDataGenerator()->create_category(array('name' => 'Cat 1'));
2555         $category2 = self::getDataGenerator()->create_category(array('parent' => $category1->id));
2556         $course1 = self::getDataGenerator()->create_course(
2557             array('category' => $category1->id, 'shortname' => 'c1', 'format' => 'topics'));
2559         $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
2560         $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
2561             'categoryid' => $fieldcategory->get('id')];
2562         $field = self::getDataGenerator()->create_custom_field($customfield);
2563         $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
2564         $course2 = self::getDataGenerator()->create_course(array('visible' => 0, 'category' => $category2->id, 'idnumber' => 'i2', 'customfields' => [$customfieldvalue]));
2566         $student1 = self::getDataGenerator()->create_user();
2567         $user1 = self::getDataGenerator()->create_user();
2568         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2569         self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id);
2570         self::getDataGenerator()->enrol_user($student1->id, $course2->id, $studentrole->id);
2572         self::setAdminUser();
2573         // As admins, we should be able to retrieve everything.
2574         $result = core_course_external::get_courses_by_field();
2575         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2576         $this->assertCount(3, $result['courses']);
2577         // Expect to receive all the fields.
2578         $this->assertCount(38, $result['courses'][0]);
2579         $this->assertCount(39, $result['courses'][1]);  // One more field because is not the site course.
2580         $this->assertCount(39, $result['courses'][2]);  // One more field because is not the site course.
2582         $result = core_course_external::get_courses_by_field('id', $course1->id);
2583         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2584         $this->assertCount(1, $result['courses']);
2585         $this->assertEquals($course1->id, $result['courses'][0]['id']);
2586         // Expect to receive all the fields.
2587         $this->assertCount(39, $result['courses'][0]);
2588         // Check default values for course format topics.
2589         $this->assertCount(2, $result['courses'][0]['courseformatoptions']);
2590         foreach ($result['courses'][0]['courseformatoptions'] as $option) {
2591             if ($option['name'] == 'hiddensections') {
2592                 $this->assertEquals(0, $option['value']);
2593             } else {
2594                 $this->assertEquals('coursedisplay', $option['name']);
2595                 $this->assertEquals(0, $option['value']);
2596             }
2597         }
2599         $result = core_course_external::get_courses_by_field('id', $course2->id);
2600         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2601         $this->assertCount(1, $result['courses']);
2602         $this->assertEquals($course2->id, $result['courses'][0]['id']);
2603         // Check custom fields properly returned.
2604         $this->assertEquals([
2605             'shortname' => $customfield['shortname'],
2606             'name' => $customfield['name'],
2607             'type' => $customfield['type'],
2608             'value' => $customfieldvalue['value'],
2609             'valueraw' => $customfieldvalue['value'],
2610         ], $result['courses'][0]['customfields'][0]);
2612         $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2613         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2614         $this->assertCount(2, $result['courses']);
2616         // Check default filters.
2617         $this->assertCount(6, $result['courses'][0]['filters']);
2618         $this->assertCount(6, $result['courses'][1]['filters']);
2620         $result = core_course_external::get_courses_by_field('category', $category1->id);
2621         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2622         $this->assertCount(1, $result['courses']);
2623         $this->assertEquals($course1->id, $result['courses'][0]['id']);
2624         $this->assertEquals('Cat 1', $result['courses'][0]['categoryname']);
2626         $result = core_course_external::get_courses_by_field('shortname', 'c1');
2627         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2628         $this->assertCount(1, $result['courses']);
2629         $this->assertEquals($course1->id, $result['courses'][0]['id']);
2631         $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2632         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2633         $this->assertCount(1, $result['courses']);
2634         $this->assertEquals($course2->id, $result['courses'][0]['id']);
2636         $result = core_course_external::get_courses_by_field('idnumber', 'x');
2637         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2638         $this->assertCount(0, $result['courses']);
2640         // Change filter value.
2641         filter_set_local_state('mediaplugin', context_course::instance($course1->id)->id, TEXTFILTER_OFF);
2643         self::setUser($student1);
2644         // All visible courses  (including front page) for normal student.
2645         $result = core_course_external::get_courses_by_field();
2646         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2647         $this->assertCount(2, $result['courses']);
2648         $this->assertCount(31, $result['courses'][0]);
2649         $this->assertCount(32, $result['courses'][1]);  // One field more (course format options), not present in site course.
2651         $result = core_course_external::get_courses_by_field('id', $course1->id);
2652         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2653         $this->assertCount(1, $result['courses']);
2654         $this->assertEquals($course1->id, $result['courses'][0]['id']);
2655         // Expect to receive all the files that a student can see.
2656         $this->assertCount(32, $result['courses'][0]);
2658         // Check default filters.
2659         $filters = $result['courses'][0]['filters'];
2660         $this->assertCount(6, $filters);
2661         $found = false;
2662         foreach ($filters as $filter) {
2663             if ($filter['filter'] == 'mediaplugin' and $filter['localstate'] == TEXTFILTER_OFF) {
2664                 $found = true;
2665             }
2666         }
2667         $this->assertTrue($found);
2669         // Course 2 is not visible.
2670         $result = core_course_external::get_courses_by_field('id', $course2->id);
2671         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2672         $this->assertCount(0, $result['courses']);
2674         $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2675         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2676         $this->assertCount(1, $result['courses']);
2678         $result = core_course_external::get_courses_by_field('category', $category1->id);
2679         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2680         $this->assertCount(1, $result['courses']);
2681         $this->assertEquals($course1->id, $result['courses'][0]['id']);
2683         $result = core_course_external::get_courses_by_field('shortname', 'c1');
2684         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2685         $this->assertCount(1, $result['courses']);
2686         $this->assertEquals($course1->id, $result['courses'][0]['id']);
2688         $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2689         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2690         $this->assertCount(0, $result['courses']);
2692         $result = core_course_external::get_courses_by_field('idnumber', 'x');
2693         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2694         $this->assertCount(0, $result['courses']);
2696         self::setUser($user1);
2697         // All visible courses (including front page) for authenticated user.
2698         $result = core_course_external::get_courses_by_field();
2699         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2700         $this->assertCount(2, $result['courses']);
2701         $this->assertCount(31, $result['courses'][0]);  // Site course.
2702         $this->assertCount(14, $result['courses'][1]);  // Only public information, not enrolled.
2704         $result = core_course_external::get_courses_by_field('id', $course1->id);
2705         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2706         $this->assertCount(1, $result['courses']);
2707         $this->assertEquals($course1->id, $result['courses'][0]['id']);
2708         // Expect to receive all the files that a authenticated can see.
2709         $this->assertCount(14, $result['courses'][0]);
2711         // Course 2 is not visible.
2712         $result = core_course_external::get_courses_by_field('id', $course2->id);
2713         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2714         $this->assertCount(0, $result['courses']);
2716         $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2717         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2718         $this->assertCount(1, $result['courses']);
2720         $result = core_course_external::get_courses_by_field('category', $category1->id);
2721         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2722         $this->assertCount(1, $result['courses']);
2723         $this->assertEquals($course1->id, $result['courses'][0]['id']);
2725         $result = core_course_external::get_courses_by_field('shortname', 'c1');
2726         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2727         $this->assertCount(1, $result['courses']);
2728         $this->assertEquals($course1->id, $result['courses'][0]['id']);
2730         $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2731         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2732         $this->assertCount(0, $result['courses']);
2734         $result = core_course_external::get_courses_by_field('idnumber', 'x');
2735         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2736         $this->assertCount(0, $result['courses']);
2737     }
2739     /**
2740      * Test retrieving courses by field returns custom field data
2741      */
2742     public function test_get_courses_by_field_customfields(): void {
2743         $this->resetAfterTest();
2744         $this->setAdminUser();
2746         $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
2747         $datefield = $this->getDataGenerator()->create_custom_field([
2748             'categoryid' => $fieldcategory->get('id'),
2749             'shortname' => 'mydate',
2750             'name' => 'My date',
2751             'type' => 'date',
2752         ]);
2754         $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
2755             [
2756                 'shortname' => $datefield->get('shortname'),
2757                 'value' => 1580389200, // 30/01/2020 13:00 GMT.
2758             ],
2759         ]]);
2761         $result = external_api::clean_returnvalue(
2762             core_course_external::get_courses_by_field_returns(),
2763             core_course_external::get_courses_by_field('id', $newcourse->id)
2764         );
2766         $this->assertCount(1, $result['courses']);
2767         $course = reset($result['courses']);
2769         $this->assertArrayHasKey('customfields', $course);
2770         $this->assertCount(1, $course['customfields']);
2772         // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
2773         $this->assertEquals([
2774             'name' => $datefield->get('name'),
2775             'shortname' => $datefield->get('shortname'),
2776             'type' => $datefield->get('type'),
2777             'value' => userdate(1580389200),
2778             'valueraw' => 1580389200,
2779         ], reset($course['customfields']));
2780     }
2782     public function test_get_courses_by_field_invalid_field() {
2783         $this->expectException('invalid_parameter_exception');
2784         $result = core_course_external::get_courses_by_field('zyx', 'x');
2785     }
2787     public function test_get_courses_by_field_invalid_courses() {
2788         $result = core_course_external::get_courses_by_field('id', '-1');
2789         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2790         $this->assertCount(0, $result['courses']);
2791     }
2793     /**
2794      * Test get_courses_by_field_invalid_theme_and_lang
2795      */
2796     public function test_get_courses_by_field_invalid_theme_and_lang() {
2797         $this->resetAfterTest(true);
2798         $this->setAdminUser();
2800         $course = self::getDataGenerator()->create_course(array('theme' => 'kkt', 'lang' => 'kkl'));
2801         $result = core_course_external::get_courses_by_field('id', $course->id);
2802         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2803         $this->assertEmpty($result['courses']['0']['theme']);
2804         $this->assertEmpty($result['courses']['0']['lang']);
2805     }
2808     public function test_check_updates() {
2809         global $DB;
2810         $this->resetAfterTest(true);
2811         $this->setAdminUser();
2813         // Create different types of activities.
2814         $course  = self::getDataGenerator()->create_course();
2815         $tocreate = array('assign', 'book', 'choice', 'folder', 'forum', 'glossary', 'imscp', 'label', 'lti', 'page', 'quiz',
2816                             'resource', 'scorm', 'survey', 'url', 'wiki');
2818         $modules = array();
2819         foreach ($tocreate as $modname) {
2820             $modules[$modname]['instance'] = $this->getDataGenerator()->create_module($modname, array('course' => $course->id));
2821             $modules[$modname]['cm'] = get_coursemodule_from_id(false, $modules[$modname]['instance']->cmid);
2822             $modules[$modname]['context'] = context_module::instance($modules[$modname]['instance']->cmid);
2823         }
2825         $student = self::getDataGenerator()->create_user();
2826         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2827         self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
2828         $this->setUser($student);
2830         $since = time();
2831         $this->waitForSecond();
2832         $params = array();
2833         foreach ($modules as $modname => $data) {
2834             $params[$data['cm']->id] = array(
2835                 'contextlevel' => 'module',
2836                 'id' => $data['cm']->id,
2837                 'since' => $since
2838             );
2839         }
2841         // Check there is nothing updated because modules are fresh new.
2842         $result = core_course_external::check_updates($course->id, $params);
2843         $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
2844         $this->assertCount(0, $result['instances']);
2845         $this->assertCount(0, $result['warnings']);
2847         // Test with get_updates_since the same data.
2848         $result = core_course_external::get_updates_since($course->id, $since);
2849         $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
2850         $this->assertCount(0, $result['instances']);
2851         $this->assertCount(0, $result['warnings']);
2853         // Update a module after a second.
2854         $this->waitForSecond();
2855         set_coursemodule_name($modules['forum']['cm']->id, 'New forum name');
2857         $found = false;
2858         $result = core_course_external::check_updates($course->id, $params);
2859         $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
2860         $this->assertCount(1, $result['instances']);
2861         $this->assertCount(0, $result['warnings']);
2862         foreach ($result['instances'] as $module) {
2863             foreach ($module['updates'] as $update) {
2864                 if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
2865                     $found = true;
2866                 }
2867             }
2868         }
2869         $this->assertTrue($found);
2871         // Test with get_updates_since the same data.
2872         $result = core_course_external::get_updates_since($course->id, $since);
2873         $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
2874         $this->assertCount(1, $result['instances']);
2875         $this->assertCount(0, $result['warnings']);
2876         $found = false;
2877         $this->assertCount(1, $result['instances']);
2878         $this->assertCount(0, $result['warnings']);
2879         foreach ($result['instances'] as $module) {
2880             foreach ($module['updates'] as $update) {
2881                 if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
2882                     $found = true;
2883                 }
2884             }
2885         }
2886         $this->assertTrue($found);
2888         // Do not retrieve the configuration field.
2889         $filter = array('files');
2890         $found = false;
2891         $result = core_course_external::check_updates($course->id, $params, $filter);
2892         $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
2893         $this->assertCount(0, $result['instances']);
2894         $this->assertCount(0, $result['warnings']);
2895         $this->assertFalse($found);
2897         // Add invalid cmid.
2898         $params[] = array(
2899             'contextlevel' => 'module',
2900             'id' => -2,
2901             'since' => $since
2902         );
2903         $result = core_course_external::check_updates($course->id, $params);
2904         $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
2905         $this->assertCount(1, $result['warnings']);
2906         $this->assertEquals(-2, $result['warnings'][0]['itemid']);
2907     }
2909     /**
2910      * Test cases for the get_enrolled_courses_by_timeline_classification test.
2911      */
2912     public function get_get_enrolled_courses_by_timeline_classification_test_cases() {
2913         $now = time();
2914         $day = 86400;
2916         $coursedata = [
2917             [
2918                 'shortname' => 'apast',
2919                 'startdate' => $now - ($day * 2),
2920                 'enddate' => $now - $day
2921             ],
2922             [
2923                 'shortname' => 'bpast',
2924                 'startdate' => $now - ($day * 2),
2925                 'enddate' => $now - $day
2926             ],
2927             [
2928                 'shortname' => 'cpast',
2929                 'startdate' => $now - ($day * 2),
2930                 'enddate' => $now - $day
2931             ],
2932             [
2933                 'shortname' => 'dpast',
2934                 'startdate' => $now - ($day * 2),
2935                 'enddate' => $now - $day
2936             ],
2937             [
2938                 'shortname' => 'epast',
2939                 'startdate' => $now - ($day * 2),
2940                 'enddate' => $now - $day
2941             ],
2942             [
2943                 'shortname' => 'ainprogress',
2944                 'startdate' => $now - $day,
2945                 'enddate' => $now + $day
2946             ],
2947             [
2948                 'shortname' => 'binprogress',
2949                 'startdate' => $now - $day,
2950                 'enddate' => $now + $day
2951             ],
2952             [
2953                 'shortname' => 'cinprogress',
2954                 'startdate' => $now - $day,
2955                 'enddate' => $now + $day
2956             ],
2957             [
2958                 'shortname' => 'dinprogress',
2959                 'startdate' => $now - $day,
2960                 'enddate' => $now + $day
2961             ],
2962             [
2963                 'shortname' => 'einprogress',
2964                 'startdate' => $now - $day,
2965                 'enddate' => $now + $day
2966             ],
2967             [
2968                 'shortname' => 'afuture',
2969                 'startdate' => $now + $day
2970             ],
2971             [
2972                 'shortname' => 'bfuture',
2973                 'startdate' => $now + $day
2974             ],
2975             [
2976                 'shortname' => 'cfuture',
2977                 'startdate' => $now + $day
2978             ],
2979             [
2980                 'shortname' => 'dfuture',
2981                 'startdate' => $now + $day
2982             ],
2983             [
2984                 'shortname' => 'efuture',
2985                 'startdate' => $now + $day
2986             ]
2987         ];
2989         // Raw enrolled courses result set should be returned in this order:
2990         // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
2991         // dfuture, dinprogress, dpast, efuture, einprogress, epast
2992         //
2993         // By classification the offset values for each record should be:
2994         // COURSE_TIMELINE_FUTURE
2995         // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
2996         // COURSE_TIMELINE_INPROGRESS
2997         // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
2998         // COURSE_TIMELINE_PAST
2999         // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
3000         //
3001         // NOTE: The offset applies to the unfiltered full set of courses before the classification
3002         // filtering is done.
3003         // E.g. In our example if an offset of 2 is given then it would mean the first
3004         // two courses (afuture, ainprogress) are ignored.
3005         return [
3006             'empty set' => [
3007                 'coursedata' => [],
3008                 'classification' => 'future',
3009                 'limit' => 2,
3010                 'offset' => 0,
3011                 'expectedcourses' => [],
3012                 'expectednextoffset' => 0
3013             ],
3014             // COURSE_TIMELINE_FUTURE.
3015             'future not limit no offset' => [
3016                 'coursedata' => $coursedata,
3017                 'classification' => 'future',
3018                 'limit' => 0,
3019                 'offset' => 0,
3020                 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3021                 'expectednextoffset' => 15
3022             ],
3023             'future no offset' => [
3024                 'coursedata' => $coursedata,
3025                 'classification' => 'future',
3026                 'limit' => 2,
3027                 'offset' => 0,
3028                 'expectedcourses' => ['afuture', 'bfuture'],
3029                 'expectednextoffset' => 4
3030             ],
3031             'future offset' => [
3032                 'coursedata' => $coursedata,
3033                 'classification' => 'future',
3034                 'limit' => 2,
3035                 'offset' => 2,
3036                 'expectedcourses' => ['bfuture', 'cfuture'],
3037                 'expectednextoffset' => 7
3038             ],
3039             'future exact limit' => [
3040                 'coursedata' => $coursedata,
3041                 'classification' => 'future',
3042                 'limit' => 5,
3043                 'offset' => 0,
3044                 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3045                 'expectednextoffset' => 13
3046             ],
3047             'future limit less results' => [
3048                 'coursedata' => $coursedata,
3049                 'classification' => 'future',
3050                 'limit' => 10,
3051                 'offset' => 0,
3052                 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3053                 'expectednextoffset' => 15
3054             ],
3055             'future limit less results with offset' => [
3056                 'coursedata' => $coursedata,
3057                 'classification' => 'future',
3058                 'limit' => 10,
3059                 'offset' => 5,
3060                 'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
3061                 'expectednextoffset' => 15
3062             ],
3063             'all no limit or offset' => [
3064                 'coursedata' => $coursedata,
3065                 'classification' => 'all',
3066                 'limit' => 0,
3067                 'offset' => 0,
3068                 'expectedcourses' => [
3069                     'afuture',
3070                     'ainprogress',
3071                     'apast',
3072                     'bfuture',
3073                     'binprogress',
3074                     'bpast',
3075                     'cfuture',
3076                     'cinprogress',
3077                     'cpast',
3078                     'dfuture',
3079                     'dinprogress',
3080                     'dpast',
3081                     'efuture',
3082                     'einprogress',
3083                     'epast'
3084                 ],
3085                 'expectednextoffset' => 15
3086             ],
3087             'all limit no offset' => [
3088                 'coursedata' => $coursedata,
3089                 'classification' => 'all',
3090                 'limit' => 5,
3091                 'offset' => 0,
3092                 'expectedcourses' => [
3093                     'afuture',
3094                     'ainprogress',
3095                     'apast',
3096                     'bfuture',
3097                     'binprogress'
3098                 ],
3099                 'expectednextoffset' => 5
3100             ],
3101             'all limit and offset' => [
3102                 'coursedata' => $coursedata,
3103                 'classification' => 'all',
3104                 'limit' => 5,
3105                 'offset' => 5,
3106                 'expectedcourses' => [
3107                     'bpast',
3108                     'cfuture',
3109                     'cinprogress',
3110                     'cpast',
3111                     'dfuture'
3112                 ],
3113                 'expectednextoffset' => 10
3114             ],
3115             'all offset past result set' => [
3116                 'coursedata' => $coursedata,
3117                 'classification' => 'all',
3118                 'limit' => 5,
3119                 'offset' => 50,
3120                 'expectedcourses' => [],
3121                 'expectednextoffset' => 50
3122             ],
3123         ];
3124     }
3126     /**
3127      * Test the get_enrolled_courses_by_timeline_classification function.
3128      *
3129      * @dataProvider get_get_enrolled_courses_by_timeline_classification_test_cases()
3130      * @param array $coursedata Courses to create
3131      * @param string $classification Timeline classification
3132      * @param int $limit Maximum number of results
3133      * @param int $offset Offset the unfiltered courses result set by this amount
3134      * @param array $expectedcourses Expected courses in result
3135      * @param int $expectednextoffset Expected next offset value in result
3136      */
3137     public function test_get_enrolled_courses_by_timeline_classification(
3138         $coursedata,
3139         $classification,
3140         $limit,
3141         $offset,
3142         $expectedcourses,
3143         $expectednextoffset
3144     ) {
3145         $this->resetAfterTest();
3146         $generator = $this->getDataGenerator();
3148         $courses = array_map(function($coursedata) use ($generator) {
3149             return $generator->create_course($coursedata);
3150         }, $coursedata);
3152         $student = $generator->create_user();
3154         foreach ($courses as $course) {
3155             $generator->enrol_user($student->id, $course->id, 'student');
3156         }
3158