Merge branch 'MDL-63303_master-deleteduserfix' of https://github.com/markn86/moodle
[moodle.git] / course / tests / externallib_test.php
blob39c187f46eb882c91246d0b2b76b45a850b93a7f
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * External course functions unit tests
20 * @package core_course
21 * @category external
22 * @copyright 2012 Jerome Mouneyrac
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 defined('MOODLE_INTERNAL') || die();
28 global $CFG;
30 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
32 /**
33 * External course functions unit tests
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
40 class core_course_externallib_testcase extends externallib_advanced_testcase {
42 /**
43 * Tests set up
45 protected function setUp() {
46 global $CFG;
47 require_once($CFG->dirroot . '/course/externallib.php');
50 /**
51 * Test create_categories
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 = 'bootstrapbase';
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)
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)
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);
129 * Test delete categories
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)
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)));
172 * Test get categories
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);
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'];
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);
303 * Test update_categories
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);
366 * Test create_courses numsections
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.
400 * Test create_courses
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 // Set the required capabilities by the external function
413 $contextid = context_system::instance()->id;
414 $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
415 $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
416 $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
418 $category = self::getDataGenerator()->create_category();
420 // Create base categories.
421 $course1['fullname'] = 'Test course 1';
422 $course1['shortname'] = 'Testcourse1';
423 $course1['categoryid'] = $category->id;
424 $course2['fullname'] = 'Test course 2';
425 $course2['shortname'] = 'Testcourse2';
426 $course2['categoryid'] = $category->id;
427 $course2['idnumber'] = 'testcourse2idnumber';
428 $course2['summary'] = 'Description for course 2';
429 $course2['summaryformat'] = FORMAT_MOODLE;
430 $course2['format'] = 'weeks';
431 $course2['showgrades'] = 1;
432 $course2['newsitems'] = 3;
433 $course2['startdate'] = 1420092000; // 01/01/2015.
434 $course2['enddate'] = 1422669600; // 01/31/2015.
435 $course2['numsections'] = 4;
436 $course2['maxbytes'] = 100000;
437 $course2['showreports'] = 1;
438 $course2['visible'] = 0;
439 $course2['hiddensections'] = 0;
440 $course2['groupmode'] = 0;
441 $course2['groupmodeforce'] = 0;
442 $course2['defaultgroupingid'] = 0;
443 $course2['enablecompletion'] = 1;
444 $course2['completionnotify'] = 1;
445 $course2['lang'] = 'en';
446 $course2['forcetheme'] = 'bootstrapbase';
447 $course2['courseformatoptions'][] = array('name' => 'automaticenddate', 'value' => 0);
448 $course3['fullname'] = 'Test course 3';
449 $course3['shortname'] = 'Testcourse3';
450 $course3['categoryid'] = $category->id;
451 $course3['format'] = 'topics';
452 $course3options = array('numsections' => 8,
453 'hiddensections' => 1,
454 'coursedisplay' => 1);
455 $course3['courseformatoptions'] = array();
456 foreach ($course3options as $key => $value) {
457 $course3['courseformatoptions'][] = array('name' => $key, 'value' => $value);
459 $courses = array($course1, $course2, $course3);
461 $createdcourses = core_course_external::create_courses($courses);
463 // We need to execute the return values cleaning process to simulate the web service server.
464 $createdcourses = external_api::clean_returnvalue(core_course_external::create_courses_returns(), $createdcourses);
466 // Check that right number of courses were created.
467 $this->assertEquals(3, count($createdcourses));
469 // Check that the courses were correctly created.
470 foreach ($createdcourses as $createdcourse) {
471 $courseinfo = course_get_format($createdcourse['id'])->get_course();
473 if ($createdcourse['shortname'] == $course2['shortname']) {
474 $this->assertEquals($courseinfo->fullname, $course2['fullname']);
475 $this->assertEquals($courseinfo->shortname, $course2['shortname']);
476 $this->assertEquals($courseinfo->category, $course2['categoryid']);
477 $this->assertEquals($courseinfo->idnumber, $course2['idnumber']);
478 $this->assertEquals($courseinfo->summary, $course2['summary']);
479 $this->assertEquals($courseinfo->summaryformat, $course2['summaryformat']);
480 $this->assertEquals($courseinfo->format, $course2['format']);
481 $this->assertEquals($courseinfo->showgrades, $course2['showgrades']);
482 $this->assertEquals($courseinfo->newsitems, $course2['newsitems']);
483 $this->assertEquals($courseinfo->startdate, $course2['startdate']);
484 $this->assertEquals($courseinfo->enddate, $course2['enddate']);
485 $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(), $course2['numsections']);
486 $this->assertEquals($courseinfo->maxbytes, $course2['maxbytes']);
487 $this->assertEquals($courseinfo->showreports, $course2['showreports']);
488 $this->assertEquals($courseinfo->visible, $course2['visible']);
489 $this->assertEquals($courseinfo->hiddensections, $course2['hiddensections']);
490 $this->assertEquals($courseinfo->groupmode, $course2['groupmode']);
491 $this->assertEquals($courseinfo->groupmodeforce, $course2['groupmodeforce']);
492 $this->assertEquals($courseinfo->defaultgroupingid, $course2['defaultgroupingid']);
493 $this->assertEquals($courseinfo->completionnotify, $course2['completionnotify']);
494 $this->assertEquals($courseinfo->lang, $course2['lang']);
495 $this->assertEquals($courseinfo->theme, $course2['forcetheme']);
497 // We enabled completion at the beginning of the test.
498 $this->assertEquals($courseinfo->enablecompletion, $course2['enablecompletion']);
500 } else if ($createdcourse['shortname'] == $course1['shortname']) {
501 $courseconfig = get_config('moodlecourse');
502 $this->assertEquals($courseinfo->fullname, $course1['fullname']);
503 $this->assertEquals($courseinfo->shortname, $course1['shortname']);
504 $this->assertEquals($courseinfo->category, $course1['categoryid']);
505 $this->assertEquals($courseinfo->summaryformat, FORMAT_HTML);
506 $this->assertEquals($courseinfo->format, $courseconfig->format);
507 $this->assertEquals($courseinfo->showgrades, $courseconfig->showgrades);
508 $this->assertEquals($courseinfo->newsitems, $courseconfig->newsitems);
509 $this->assertEquals($courseinfo->maxbytes, $courseconfig->maxbytes);
510 $this->assertEquals($courseinfo->showreports, $courseconfig->showreports);
511 $this->assertEquals($courseinfo->groupmode, $courseconfig->groupmode);
512 $this->assertEquals($courseinfo->groupmodeforce, $courseconfig->groupmodeforce);
513 $this->assertEquals($courseinfo->defaultgroupingid, 0);
514 } else if ($createdcourse['shortname'] == $course3['shortname']) {
515 $this->assertEquals($courseinfo->fullname, $course3['fullname']);
516 $this->assertEquals($courseinfo->shortname, $course3['shortname']);
517 $this->assertEquals($courseinfo->category, $course3['categoryid']);
518 $this->assertEquals($courseinfo->format, $course3['format']);
519 $this->assertEquals($courseinfo->hiddensections, $course3options['hiddensections']);
520 $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(),
521 $course3options['numsections']);
522 $this->assertEquals($courseinfo->coursedisplay, $course3options['coursedisplay']);
523 } else {
524 throw new moodle_exception('Unexpected shortname');
528 // Call without required capability
529 $this->unassignUserCapability('moodle/course:create', $contextid, $roleid);
530 $this->expectException('required_capability_exception');
531 $createdsubcats = core_course_external::create_courses($courses);
535 * Test delete_courses
537 public function test_delete_courses() {
538 global $DB, $USER;
540 $this->resetAfterTest(true);
542 // Admin can delete a course.
543 $this->setAdminUser();
544 // Validate_context() will fail as the email is not set by $this->setAdminUser().
545 $USER->email = 'emailtopass@example.com';
547 $course1 = self::getDataGenerator()->create_course();
548 $course2 = self::getDataGenerator()->create_course();
549 $course3 = self::getDataGenerator()->create_course();
551 // Delete courses.
552 $result = core_course_external::delete_courses(array($course1->id, $course2->id));
553 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
554 // Check for 0 warnings.
555 $this->assertEquals(0, count($result['warnings']));
557 // Check $course 1 and 2 are deleted.
558 $notdeletedcount = $DB->count_records_select('course',
559 'id IN ( ' . $course1->id . ',' . $course2->id . ')');
560 $this->assertEquals(0, $notdeletedcount);
562 // Try to delete non-existent course.
563 $result = core_course_external::delete_courses(array($course1->id));
564 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
565 // Check for 1 warnings.
566 $this->assertEquals(1, count($result['warnings']));
568 // Try to delete Frontpage course.
569 $result = core_course_external::delete_courses(array(0));
570 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
571 // Check for 1 warnings.
572 $this->assertEquals(1, count($result['warnings']));
574 // Fail when the user has access to course (enrolled) but does not have permission or is not admin.
575 $student1 = self::getDataGenerator()->create_user();
576 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
577 $this->getDataGenerator()->enrol_user($student1->id,
578 $course3->id,
579 $studentrole->id);
580 $this->setUser($student1);
581 $result = core_course_external::delete_courses(array($course3->id));
582 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
583 // Check for 1 warnings.
584 $this->assertEquals(1, count($result['warnings']));
586 // Fail when the user is not allow to access the course (enrolled) or is not admin.
587 $this->setGuestUser();
588 $this->expectException('require_login_exception');
590 $result = core_course_external::delete_courses(array($course3->id));
591 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
595 * Test get_courses
597 public function test_get_courses () {
598 global $DB;
600 $this->resetAfterTest(true);
602 $generatedcourses = array();
603 $coursedata['idnumber'] = 'idnumbercourse1';
604 // Adding tags here to check that format_string is applied.
605 $coursedata['fullname'] = '<b>Course 1 for PHPunit test</b>';
606 $coursedata['shortname'] = '<b>Course 1 for PHPunit test</b>';
607 $coursedata['summary'] = 'Course 1 description';
608 $coursedata['summaryformat'] = FORMAT_MOODLE;
609 $course1 = self::getDataGenerator()->create_course($coursedata);
611 $generatedcourses[$course1->id] = $course1;
612 $course2 = self::getDataGenerator()->create_course();
613 $generatedcourses[$course2->id] = $course2;
614 $course3 = self::getDataGenerator()->create_course(array('format' => 'topics'));
615 $generatedcourses[$course3->id] = $course3;
617 // Set the required capabilities by the external function.
618 $context = context_system::instance();
619 $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
620 $this->assignUserCapability('moodle/course:update',
621 context_course::instance($course1->id)->id, $roleid);
622 $this->assignUserCapability('moodle/course:update',
623 context_course::instance($course2->id)->id, $roleid);
624 $this->assignUserCapability('moodle/course:update',
625 context_course::instance($course3->id)->id, $roleid);
627 $courses = core_course_external::get_courses(array('ids' =>
628 array($course1->id, $course2->id)));
630 // We need to execute the return values cleaning process to simulate the web service server.
631 $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
633 // Check we retrieve the good total number of categories.
634 $this->assertEquals(2, count($courses));
636 foreach ($courses as $course) {
637 $coursecontext = context_course::instance($course['id']);
638 $dbcourse = $generatedcourses[$course['id']];
639 $this->assertEquals($course['idnumber'], $dbcourse->idnumber);
640 $this->assertEquals($course['fullname'], external_format_string($dbcourse->fullname, $coursecontext->id));
641 $this->assertEquals($course['displayname'], external_format_string(get_course_display_name_for_list($dbcourse),
642 $coursecontext->id));
643 // Summary was converted to the HTML format.
644 $this->assertEquals($course['summary'], format_text($dbcourse->summary, FORMAT_MOODLE, array('para' => false)));
645 $this->assertEquals($course['summaryformat'], FORMAT_HTML);
646 $this->assertEquals($course['shortname'], external_format_string($dbcourse->shortname, $coursecontext->id));
647 $this->assertEquals($course['categoryid'], $dbcourse->category);
648 $this->assertEquals($course['format'], $dbcourse->format);
649 $this->assertEquals($course['showgrades'], $dbcourse->showgrades);
650 $this->assertEquals($course['newsitems'], $dbcourse->newsitems);
651 $this->assertEquals($course['startdate'], $dbcourse->startdate);
652 $this->assertEquals($course['enddate'], $dbcourse->enddate);
653 $this->assertEquals($course['numsections'], course_get_format($dbcourse)->get_last_section_number());
654 $this->assertEquals($course['maxbytes'], $dbcourse->maxbytes);
655 $this->assertEquals($course['showreports'], $dbcourse->showreports);
656 $this->assertEquals($course['visible'], $dbcourse->visible);
657 $this->assertEquals($course['hiddensections'], $dbcourse->hiddensections);
658 $this->assertEquals($course['groupmode'], $dbcourse->groupmode);
659 $this->assertEquals($course['groupmodeforce'], $dbcourse->groupmodeforce);
660 $this->assertEquals($course['defaultgroupingid'], $dbcourse->defaultgroupingid);
661 $this->assertEquals($course['completionnotify'], $dbcourse->completionnotify);
662 $this->assertEquals($course['lang'], $dbcourse->lang);
663 $this->assertEquals($course['forcetheme'], $dbcourse->theme);
664 $this->assertEquals($course['enablecompletion'], $dbcourse->enablecompletion);
665 if ($dbcourse->format === 'topics') {
666 $this->assertEquals($course['courseformatoptions'], array(
667 array('name' => 'hiddensections', 'value' => $dbcourse->hiddensections),
668 array('name' => 'coursedisplay', 'value' => $dbcourse->coursedisplay),
673 // Get all courses in the DB
674 $courses = core_course_external::get_courses(array());
676 // We need to execute the return values cleaning process to simulate the web service server.
677 $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
679 $this->assertEquals($DB->count_records('course'), count($courses));
683 * Test get_courses without capability
685 public function test_get_courses_without_capability() {
686 $this->resetAfterTest(true);
688 $course1 = $this->getDataGenerator()->create_course();
689 $this->setUser($this->getDataGenerator()->create_user());
691 // No permissions are required to get the site course.
692 $courses = core_course_external::get_courses(array('ids' => [SITEID]));
693 $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
695 $this->assertEquals(1, count($courses));
696 $this->assertEquals('PHPUnit test site', $courses[0]['fullname']);
697 $this->assertEquals('site', $courses[0]['format']);
699 // Requesting course without being enrolled or capability to view it will throw an exception.
700 try {
701 core_course_external::get_courses(array('ids' => [$course1->id]));
702 $this->fail('Exception expected');
703 } catch (moodle_exception $e) {
704 $this->assertEquals(1, preg_match('/Course or activity not accessible. \(Not enrolled\)/', $e->getMessage()));
709 * Test search_courses
711 public function test_search_courses () {
713 global $DB;
715 $this->resetAfterTest(true);
716 $this->setAdminUser();
717 $generatedcourses = array();
718 $coursedata1['fullname'] = 'FIRST COURSE';
719 $course1 = self::getDataGenerator()->create_course($coursedata1);
721 $page = new moodle_page();
722 $page->set_course($course1);
723 $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
725 $coursedata2['fullname'] = 'SECOND COURSE';
726 $course2 = self::getDataGenerator()->create_course($coursedata2);
728 $page = new moodle_page();
729 $page->set_course($course2);
730 $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
731 // Search by name.
732 $results = core_course_external::search_courses('search', 'FIRST');
733 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
734 $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
735 $this->assertCount(1, $results['courses']);
737 // Create the forum.
738 $record = new stdClass();
739 $record->introformat = FORMAT_HTML;
740 $record->course = $course2->id;
741 // Set Aggregate type = Average of ratings.
742 $forum = self::getDataGenerator()->create_module('forum', $record);
744 // Search by module.
745 $results = core_course_external::search_courses('modulelist', 'forum');
746 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
747 $this->assertEquals(1, $results['total']);
749 // Enable coursetag option.
750 set_config('block_tags_showcoursetags', true);
751 // Add tag 'TAG-LABEL ON SECOND COURSE' to Course2.
752 core_tag_tag::set_item_tags('core', 'course', $course2->id, context_course::instance($course2->id),
753 array('TAG-LABEL ON SECOND COURSE'));
754 $taginstance = $DB->get_record('tag_instance',
755 array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST);
756 // Search by tagid.
757 $results = core_course_external::search_courses('tagid', $taginstance->tagid);
758 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
759 $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
761 // Search by block (use news_items default block).
762 $blockid = $DB->get_field('block', 'id', array('name' => 'news_items'));
763 $results = core_course_external::search_courses('blocklist', $blockid);
764 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
765 $this->assertEquals(2, $results['total']);
767 // Now as a normal user.
768 $user = self::getDataGenerator()->create_user();
770 // Add a 3rd, hidden, course we shouldn't see, even when enrolled as student.
771 $coursedata3['fullname'] = 'HIDDEN COURSE';
772 $coursedata3['visible'] = 0;
773 $course3 = self::getDataGenerator()->create_course($coursedata3);
774 $this->getDataGenerator()->enrol_user($user->id, $course3->id, 'student');
776 $this->getDataGenerator()->enrol_user($user->id, $course2->id, 'student');
777 $this->setUser($user);
779 $results = core_course_external::search_courses('search', 'FIRST');
780 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
781 $this->assertCount(1, $results['courses']);
782 $this->assertEquals(1, $results['total']);
783 $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
785 // Check that we can see both without the limit to enrolled setting.
786 $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 0);
787 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
788 $this->assertCount(2, $results['courses']);
789 $this->assertEquals(2, $results['total']);
791 // Check that we only see our enrolled course when limiting.
792 $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 1);
793 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
794 $this->assertCount(1, $results['courses']);
795 $this->assertEquals(1, $results['total']);
796 $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
798 // Search by block (use news_items default block). Should fail (only admins allowed).
799 $this->expectException('required_capability_exception');
800 $results = core_course_external::search_courses('blocklist', $blockid);
805 * Create a course with contents
806 * @return array A list with the course object and course modules objects
808 private function prepare_get_course_contents_test() {
809 global $DB, $CFG;
811 $CFG->allowstealth = 1; // Allow stealth activities.
812 $CFG->enablecompletion = true;
813 $course = self::getDataGenerator()->create_course(['numsections' => 4, 'enablecompletion' => 1]);
815 $forumdescription = 'This is the forum description';
816 $forum = $this->getDataGenerator()->create_module('forum',
817 array('course' => $course->id, 'intro' => $forumdescription, 'trackingtype' => 2),
818 array('showdescription' => true, 'completion' => COMPLETION_TRACKING_MANUAL));
819 $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
820 // Add discussions to the tracking forced forum.
821 $record = new stdClass();
822 $record->course = $course->id;
823 $record->userid = 0;
824 $record->forum = $forum->id;
825 $discussionforce = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
826 $data = $this->getDataGenerator()->create_module('data',
827 array('assessed' => 1, 'scale' => 100, 'course' => $course->id, 'completion' => 2, 'completionentries' => 3));
828 $datacm = get_coursemodule_from_instance('data', $data->id);
829 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
830 $pagecm = get_coursemodule_from_instance('page', $page->id);
831 // This is an stealth page (set by visibleoncoursepage).
832 $pagestealth = $this->getDataGenerator()->create_module('page', array('course' => $course->id, 'visibleoncoursepage' => 0));
833 $labeldescription = 'This is a very long label to test if more than 50 characters are returned.
834 So bla bla bla bla <b>bold bold bold</b> bla bla bla bla.';
835 $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
836 'intro' => $labeldescription));
837 $labelcm = get_coursemodule_from_instance('label', $label->id);
838 $tomorrow = time() + DAYSECS;
839 // Module with availability restrictions not met.
840 $url = $this->getDataGenerator()->create_module('url',
841 array('course' => $course->id, 'name' => 'URL: % & $ ../', 'section' => 2, 'display' => RESOURCELIB_DISPLAY_POPUP,
842 'popupwidth' => 100, 'popupheight' => 100),
843 array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}'));
844 $urlcm = get_coursemodule_from_instance('url', $url->id);
845 // Module for the last section.
846 $this->getDataGenerator()->create_module('url',
847 array('course' => $course->id, 'name' => 'URL for last section', 'section' => 3));
848 // Module for section 1 with availability restrictions met.
849 $yesterday = time() - DAYSECS;
850 $this->getDataGenerator()->create_module('url',
851 array('course' => $course->id, 'name' => 'URL restrictions met', 'section' => 1),
852 array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":'. $yesterday .'}],"showc":[true]}'));
854 // Set the required capabilities by the external function.
855 $context = context_course::instance($course->id);
856 $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
857 $this->assignUserCapability('moodle/course:update', $context->id, $roleid);
858 $this->assignUserCapability('mod/data:view', $context->id, $roleid);
860 $conditions = array('course' => $course->id, 'section' => 2);
861 $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
863 // Add date availability condition not met for section 3.
864 $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}';
865 $DB->set_field('course_sections', 'availability', $availability,
866 array('course' => $course->id, 'section' => 3));
868 // Create resource for last section.
869 $pageinhiddensection = $this->getDataGenerator()->create_module('page',
870 array('course' => $course->id, 'name' => 'Page in hidden section', 'section' => 4));
871 // Set not visible last section.
872 $DB->set_field('course_sections', 'visible', 0,
873 array('course' => $course->id, 'section' => 4));
875 rebuild_course_cache($course->id, true);
877 return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm);
881 * Test get_course_contents
883 public function test_get_course_contents() {
884 global $CFG;
885 $this->resetAfterTest(true);
887 $CFG->forum_allowforcedreadtracking = 1;
888 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
890 // We first run the test as admin.
891 $this->setAdminUser();
892 $sections = core_course_external::get_course_contents($course->id, array());
893 // We need to execute the return values cleaning process to simulate the web service server.
894 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
896 $modinfo = get_fast_modinfo($course);
897 $testexecuted = 0;
898 foreach ($sections[0]['modules'] as $module) {
899 if ($module['id'] == $forumcm->id and $module['modname'] == 'forum') {
900 $cm = $modinfo->cms[$forumcm->id];
901 $formattedtext = format_text($cm->content, FORMAT_HTML,
902 array('noclean' => true, 'para' => false, 'filter' => false));
903 $this->assertEquals($formattedtext, $module['description']);
904 $this->assertEquals($forumcm->instance, $module['instance']);
905 $this->assertContains('1 unread post', $module['afterlink']);
906 $testexecuted = $testexecuted + 2;
907 } else if ($module['id'] == $labelcm->id and $module['modname'] == 'label') {
908 $cm = $modinfo->cms[$labelcm->id];
909 $formattedtext = format_text($cm->content, FORMAT_HTML,
910 array('noclean' => true, 'para' => false, 'filter' => false));
911 $this->assertEquals($formattedtext, $module['description']);
912 $this->assertEquals($labelcm->instance, $module['instance']);
913 $testexecuted = $testexecuted + 1;
914 } else if ($module['id'] == $datacm->id and $module['modname'] == 'data') {
915 $this->assertContains('customcompletionrules', $module['customdata']);
916 $testexecuted = $testexecuted + 1;
919 foreach ($sections[2]['modules'] as $module) {
920 if ($module['id'] == $urlcm->id and $module['modname'] == 'url') {
921 $this->assertContains('width=100,height=100', $module['onclick']);
922 $testexecuted = $testexecuted + 1;
926 $CFG->forum_allowforcedreadtracking = 0; // Recover original value.
927 forum_tp_count_forum_unread_posts($forumcm, $course, true); // Reset static cache for further tests.
929 $this->assertEquals(5, $testexecuted);
930 $this->assertEquals(0, $sections[0]['section']);
932 $this->assertCount(5, $sections[0]['modules']);
933 $this->assertCount(1, $sections[1]['modules']);
934 $this->assertCount(1, $sections[2]['modules']);
935 $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions.
936 $this->assertCount(1, $sections[4]['modules']); // One module for the hidden section with a visible activity.
937 $this->assertNotEmpty($sections[3]['availabilityinfo']);
938 $this->assertEquals(1, $sections[1]['section']);
939 $this->assertEquals(2, $sections[2]['section']);
940 $this->assertEquals(3, $sections[3]['section']);
941 $this->assertEquals(4, $sections[4]['section']);
942 $this->assertContains('<iframe', $sections[2]['summary']);
943 $this->assertContains('</iframe>', $sections[2]['summary']);
944 $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']);
945 try {
946 $sections = core_course_external::get_course_contents($course->id,
947 array(array("name" => "invalid", "value" => 1)));
948 $this->fail('Exception expected due to invalid option.');
949 } catch (moodle_exception $e) {
950 $this->assertEquals('errorinvalidparam', $e->errorcode);
956 * Test get_course_contents as student
958 public function test_get_course_contents_student() {
959 global $DB;
960 $this->resetAfterTest(true);
962 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
964 $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
965 $user = self::getDataGenerator()->create_user();
966 self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
967 $this->setUser($user);
969 $sections = core_course_external::get_course_contents($course->id, array());
970 // We need to execute the return values cleaning process to simulate the web service server.
971 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
973 $this->assertCount(4, $sections); // Nothing for the not visible section.
974 $this->assertCount(5, $sections[0]['modules']);
975 $this->assertCount(1, $sections[1]['modules']);
976 $this->assertCount(1, $sections[2]['modules']);
977 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
979 $this->assertNotEmpty($sections[3]['availabilityinfo']);
980 $this->assertEquals(1, $sections[1]['section']);
981 $this->assertEquals(2, $sections[2]['section']);
982 $this->assertEquals(3, $sections[3]['section']);
983 // The module with the availability restriction met is returning contents.
984 $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
985 // The module with the availability restriction not met is not returning contents.
986 $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
988 // Now include flag for returning stealth information (fake section).
989 $sections = core_course_external::get_course_contents($course->id,
990 array(array("name" => "includestealthmodules", "value" => 1)));
991 // We need to execute the return values cleaning process to simulate the web service server.
992 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
994 $this->assertCount(5, $sections); // Include fake section with stealth activities.
995 $this->assertCount(5, $sections[0]['modules']);
996 $this->assertCount(1, $sections[1]['modules']);
997 $this->assertCount(1, $sections[2]['modules']);
998 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
999 $this->assertCount(1, $sections[4]['modules']); // One stealh module.
1000 $this->assertEquals(-1, $sections[4]['id']);
1004 * Test get_course_contents excluding modules
1006 public function test_get_course_contents_excluding_modules() {
1007 $this->resetAfterTest(true);
1009 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1011 // Test exclude modules.
1012 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludemodules", "value" => 1)));
1014 // We need to execute the return values cleaning process to simulate the web service server.
1015 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1017 $this->assertEmpty($sections[0]['modules']);
1018 $this->assertEmpty($sections[1]['modules']);
1022 * Test get_course_contents excluding contents
1024 public function test_get_course_contents_excluding_contents() {
1025 $this->resetAfterTest(true);
1027 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1029 // Test exclude modules.
1030 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludecontents", "value" => 1)));
1032 // We need to execute the return values cleaning process to simulate the web service server.
1033 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1035 foreach ($sections as $section) {
1036 foreach ($section['modules'] as $module) {
1037 // Only resources return contents.
1038 if (isset($module['contents'])) {
1039 $this->assertEmpty($module['contents']);
1046 * Test get_course_contents filtering by section number
1048 public function test_get_course_contents_section_number() {
1049 $this->resetAfterTest(true);
1051 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1053 // Test exclude modules.
1054 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "sectionnumber", "value" => 0)));
1056 // We need to execute the return values cleaning process to simulate the web service server.
1057 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1059 $this->assertCount(1, $sections);
1060 $this->assertCount(5, $sections[0]['modules']);
1064 * Test get_course_contents filtering by cmid
1066 public function test_get_course_contents_cmid() {
1067 $this->resetAfterTest(true);
1069 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1071 // Test exclude modules.
1072 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "cmid", "value" => $forumcm->id)));
1074 // We need to execute the return values cleaning process to simulate the web service server.
1075 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1077 $this->assertCount(4, $sections);
1078 $this->assertCount(1, $sections[0]['modules']);
1079 $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1084 * Test get_course_contents filtering by cmid and section
1086 public function test_get_course_contents_section_cmid() {
1087 $this->resetAfterTest(true);
1089 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1091 // Test exclude modules.
1092 $sections = core_course_external::get_course_contents($course->id, array(
1093 array("name" => "cmid", "value" => $forumcm->id),
1094 array("name" => "sectionnumber", "value" => 0)
1097 // We need to execute the return values cleaning process to simulate the web service server.
1098 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1100 $this->assertCount(1, $sections);
1101 $this->assertCount(1, $sections[0]['modules']);
1102 $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1106 * Test get_course_contents filtering by modname
1108 public function test_get_course_contents_modname() {
1109 $this->resetAfterTest(true);
1111 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1113 // Test exclude modules.
1114 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "modname", "value" => "forum")));
1116 // We need to execute the return values cleaning process to simulate the web service server.
1117 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1119 $this->assertCount(4, $sections);
1120 $this->assertCount(1, $sections[0]['modules']);
1121 $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1125 * Test get_course_contents filtering by modname
1127 public function test_get_course_contents_modid() {
1128 $this->resetAfterTest(true);
1130 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1132 // Test exclude modules.
1133 $sections = core_course_external::get_course_contents($course->id, array(
1134 array("name" => "modname", "value" => "page"),
1135 array("name" => "modid", "value" => $pagecm->instance),
1138 // We need to execute the return values cleaning process to simulate the web service server.
1139 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1141 $this->assertCount(4, $sections);
1142 $this->assertCount(1, $sections[0]['modules']);
1143 $this->assertEquals("page", $sections[0]['modules'][0]["modname"]);
1144 $this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]);
1148 * Test get course contents completion
1150 public function test_get_course_contents_completion() {
1151 global $CFG;
1152 $this->resetAfterTest(true);
1154 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1156 // Test activity not completed yet.
1157 $result = core_course_external::get_course_contents($course->id, array(
1158 array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1159 // We need to execute the return values cleaning process to simulate the web service server.
1160 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1162 $this->assertCount(1, $result[0]['modules']);
1163 $this->assertEquals("forum", $result[0]['modules'][0]["modname"]);
1164 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1165 $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['state']);
1166 $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['timecompleted']);
1167 $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']);
1169 // Set activity completed.
1170 core_completion_external::update_activity_completion_status_manually($forumcm->id, true);
1172 $result = core_course_external::get_course_contents($course->id, array(
1173 array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1174 // We need to execute the return values cleaning process to simulate the web service server.
1175 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1177 $this->assertEquals(COMPLETION_COMPLETE, $result[0]['modules'][0]["completiondata"]['state']);
1178 $this->assertNotEmpty($result[0]['modules'][0]["completiondata"]['timecompleted']);
1179 $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']);
1181 // Disable completion.
1182 $CFG->enablecompletion = 0;
1183 $result = core_course_external::get_course_contents($course->id, array(
1184 array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1185 // We need to execute the return values cleaning process to simulate the web service server.
1186 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1188 $this->assertArrayNotHasKey('completiondata', $result[0]['modules'][0]);
1192 * Test duplicate_course
1194 public function test_duplicate_course() {
1195 $this->resetAfterTest(true);
1197 // Create one course with three modules.
1198 $course = self::getDataGenerator()->create_course();
1199 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
1200 $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1201 $forumcontext = context_module::instance($forum->cmid);
1202 $data = $this->getDataGenerator()->create_module('data', array('assessed'=>1, 'scale'=>100, 'course'=>$course->id));
1203 $datacontext = context_module::instance($data->cmid);
1204 $datacm = get_coursemodule_from_instance('page', $data->id);
1205 $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
1206 $pagecontext = context_module::instance($page->cmid);
1207 $pagecm = get_coursemodule_from_instance('page', $page->id);
1209 // Set the required capabilities by the external function.
1210 $coursecontext = context_course::instance($course->id);
1211 $categorycontext = context_coursecat::instance($course->category);
1212 $roleid = $this->assignUserCapability('moodle/course:create', $categorycontext->id);
1213 $this->assignUserCapability('moodle/course:view', $categorycontext->id, $roleid);
1214 $this->assignUserCapability('moodle/restore:restorecourse', $categorycontext->id, $roleid);
1215 $this->assignUserCapability('moodle/backup:backupcourse', $coursecontext->id, $roleid);
1216 $this->assignUserCapability('moodle/backup:configure', $coursecontext->id, $roleid);
1217 // Optional capabilities to copy user data.
1218 $this->assignUserCapability('moodle/backup:userinfo', $coursecontext->id, $roleid);
1219 $this->assignUserCapability('moodle/restore:userinfo', $categorycontext->id, $roleid);
1221 $newcourse['fullname'] = 'Course duplicate';
1222 $newcourse['shortname'] = 'courseduplicate';
1223 $newcourse['categoryid'] = $course->category;
1224 $newcourse['visible'] = true;
1225 $newcourse['options'][] = array('name' => 'users', 'value' => true);
1227 $duplicate = core_course_external::duplicate_course($course->id, $newcourse['fullname'],
1228 $newcourse['shortname'], $newcourse['categoryid'], $newcourse['visible'], $newcourse['options']);
1230 // We need to execute the return values cleaning process to simulate the web service server.
1231 $duplicate = external_api::clean_returnvalue(core_course_external::duplicate_course_returns(), $duplicate);
1233 // Check that the course has been duplicated.
1234 $this->assertEquals($newcourse['shortname'], $duplicate['shortname']);
1238 * Test update_courses
1240 public function test_update_courses() {
1241 global $DB, $CFG, $USER, $COURSE;
1243 // Get current $COURSE to be able to restore it later (defaults to $SITE). We need this
1244 // trick because we are both updating and getting (for testing) course information
1245 // in the same request and core_course_external::update_courses()
1246 // is overwriting $COURSE all over the time with OLD values, so later
1247 // use of get_course() fetches those OLD values instead of the updated ones.
1248 // See MDL-39723 for more info.
1249 $origcourse = clone($COURSE);
1251 $this->resetAfterTest(true);
1253 // Set the required capabilities by the external function.
1254 $contextid = context_system::instance()->id;
1255 $roleid = $this->assignUserCapability('moodle/course:update', $contextid);
1256 $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1257 $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1258 $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
1259 $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
1260 $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
1261 $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
1262 $this->assignUserCapability('moodle/course:viewhiddencourses', $contextid, $roleid);
1263 $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
1265 // Create category and course.
1266 $category1 = self::getDataGenerator()->create_category();
1267 $category2 = self::getDataGenerator()->create_category();
1268 $originalcourse1 = self::getDataGenerator()->create_course();
1269 self::getDataGenerator()->enrol_user($USER->id, $originalcourse1->id, $roleid);
1270 $originalcourse2 = self::getDataGenerator()->create_course();
1271 self::getDataGenerator()->enrol_user($USER->id, $originalcourse2->id, $roleid);
1273 // Course values to be updated.
1274 $course1['id'] = $originalcourse1->id;
1275 $course1['fullname'] = 'Updated test course 1';
1276 $course1['shortname'] = 'Udestedtestcourse1';
1277 $course1['categoryid'] = $category1->id;
1278 $course2['id'] = $originalcourse2->id;
1279 $course2['fullname'] = 'Updated test course 2';
1280 $course2['shortname'] = 'Updestedtestcourse2';
1281 $course2['categoryid'] = $category2->id;
1282 $course2['idnumber'] = 'Updatedidnumber2';
1283 $course2['summary'] = 'Updaated description for course 2';
1284 $course2['summaryformat'] = FORMAT_HTML;
1285 $course2['format'] = 'topics';
1286 $course2['showgrades'] = 1;
1287 $course2['newsitems'] = 3;
1288 $course2['startdate'] = 1420092000; // 01/01/2015.
1289 $course2['enddate'] = 1422669600; // 01/31/2015.
1290 $course2['maxbytes'] = 100000;
1291 $course2['showreports'] = 1;
1292 $course2['visible'] = 0;
1293 $course2['hiddensections'] = 0;
1294 $course2['groupmode'] = 0;
1295 $course2['groupmodeforce'] = 0;
1296 $course2['defaultgroupingid'] = 0;
1297 $course2['enablecompletion'] = 1;
1298 $course2['lang'] = 'en';
1299 $course2['forcetheme'] = 'bootstrapbase';
1300 $courses = array($course1, $course2);
1302 $updatedcoursewarnings = core_course_external::update_courses($courses);
1303 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1304 $updatedcoursewarnings);
1305 $COURSE = $origcourse; // Restore $COURSE. Instead of using the OLD one set by the previous line.
1307 // Check that right number of courses were created.
1308 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1310 // Check that the courses were correctly created.
1311 foreach ($courses as $course) {
1312 $courseinfo = course_get_format($course['id'])->get_course();
1313 if ($course['id'] == $course2['id']) {
1314 $this->assertEquals($course2['fullname'], $courseinfo->fullname);
1315 $this->assertEquals($course2['shortname'], $courseinfo->shortname);
1316 $this->assertEquals($course2['categoryid'], $courseinfo->category);
1317 $this->assertEquals($course2['idnumber'], $courseinfo->idnumber);
1318 $this->assertEquals($course2['summary'], $courseinfo->summary);
1319 $this->assertEquals($course2['summaryformat'], $courseinfo->summaryformat);
1320 $this->assertEquals($course2['format'], $courseinfo->format);
1321 $this->assertEquals($course2['showgrades'], $courseinfo->showgrades);
1322 $this->assertEquals($course2['newsitems'], $courseinfo->newsitems);
1323 $this->assertEquals($course2['startdate'], $courseinfo->startdate);
1324 $this->assertEquals($course2['enddate'], $courseinfo->enddate);
1325 $this->assertEquals($course2['maxbytes'], $courseinfo->maxbytes);
1326 $this->assertEquals($course2['showreports'], $courseinfo->showreports);
1327 $this->assertEquals($course2['visible'], $courseinfo->visible);
1328 $this->assertEquals($course2['hiddensections'], $courseinfo->hiddensections);
1329 $this->assertEquals($course2['groupmode'], $courseinfo->groupmode);
1330 $this->assertEquals($course2['groupmodeforce'], $courseinfo->groupmodeforce);
1331 $this->assertEquals($course2['defaultgroupingid'], $courseinfo->defaultgroupingid);
1332 $this->assertEquals($course2['lang'], $courseinfo->lang);
1334 if (!empty($CFG->allowcoursethemes)) {
1335 $this->assertEquals($course2['forcetheme'], $courseinfo->theme);
1338 $this->assertEquals($course2['enablecompletion'], $courseinfo->enablecompletion);
1339 } else if ($course['id'] == $course1['id']) {
1340 $this->assertEquals($course1['fullname'], $courseinfo->fullname);
1341 $this->assertEquals($course1['shortname'], $courseinfo->shortname);
1342 $this->assertEquals($course1['categoryid'], $courseinfo->category);
1343 $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
1344 $this->assertEquals('topics', $courseinfo->format);
1345 $this->assertEquals(5, course_get_format($course['id'])->get_last_section_number());
1346 $this->assertEquals(0, $courseinfo->newsitems);
1347 $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
1348 } else {
1349 throw new moodle_exception('Unexpected shortname');
1353 $courses = array($course1);
1354 // Try update course without update capability.
1355 $user = self::getDataGenerator()->create_user();
1356 $this->setUser($user);
1357 $this->unassignUserCapability('moodle/course:update', $contextid, $roleid);
1358 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1359 $updatedcoursewarnings = core_course_external::update_courses($courses);
1360 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1361 $updatedcoursewarnings);
1362 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1364 // Try update course category without capability.
1365 $this->assignUserCapability('moodle/course:update', $contextid, $roleid);
1366 $this->unassignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1367 $user = self::getDataGenerator()->create_user();
1368 $this->setUser($user);
1369 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1370 $course1['categoryid'] = $category2->id;
1371 $courses = array($course1);
1372 $updatedcoursewarnings = core_course_external::update_courses($courses);
1373 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1374 $updatedcoursewarnings);
1375 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1377 // Try update course fullname without capability.
1378 $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1379 $this->unassignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1380 $user = self::getDataGenerator()->create_user();
1381 $this->setUser($user);
1382 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1383 $updatedcoursewarnings = core_course_external::update_courses($courses);
1384 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1385 $updatedcoursewarnings);
1386 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1387 $course1['fullname'] = 'Testing fullname without permission';
1388 $courses = array($course1);
1389 $updatedcoursewarnings = core_course_external::update_courses($courses);
1390 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1391 $updatedcoursewarnings);
1392 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1394 // Try update course shortname without capability.
1395 $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1396 $this->unassignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
1397 $user = self::getDataGenerator()->create_user();
1398 $this->setUser($user);
1399 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1400 $updatedcoursewarnings = core_course_external::update_courses($courses);
1401 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1402 $updatedcoursewarnings);
1403 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1404 $course1['shortname'] = 'Testing shortname without permission';
1405 $courses = array($course1);
1406 $updatedcoursewarnings = core_course_external::update_courses($courses);
1407 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1408 $updatedcoursewarnings);
1409 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1411 // Try update course idnumber without capability.
1412 $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
1413 $this->unassignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
1414 $user = self::getDataGenerator()->create_user();
1415 $this->setUser($user);
1416 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1417 $updatedcoursewarnings = core_course_external::update_courses($courses);
1418 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1419 $updatedcoursewarnings);
1420 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1421 $course1['idnumber'] = 'NEWIDNUMBER';
1422 $courses = array($course1);
1423 $updatedcoursewarnings = core_course_external::update_courses($courses);
1424 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1425 $updatedcoursewarnings);
1426 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1428 // Try update course summary without capability.
1429 $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
1430 $this->unassignUserCapability('moodle/course:changesummary', $contextid, $roleid);
1431 $user = self::getDataGenerator()->create_user();
1432 $this->setUser($user);
1433 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1434 $updatedcoursewarnings = core_course_external::update_courses($courses);
1435 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1436 $updatedcoursewarnings);
1437 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1438 $course1['summary'] = 'New summary';
1439 $courses = array($course1);
1440 $updatedcoursewarnings = core_course_external::update_courses($courses);
1441 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1442 $updatedcoursewarnings);
1443 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1445 // Try update course with invalid summary format.
1446 $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
1447 $user = self::getDataGenerator()->create_user();
1448 $this->setUser($user);
1449 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1450 $updatedcoursewarnings = core_course_external::update_courses($courses);
1451 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1452 $updatedcoursewarnings);
1453 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1454 $course1['summaryformat'] = 10;
1455 $courses = array($course1);
1456 $updatedcoursewarnings = core_course_external::update_courses($courses);
1457 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1458 $updatedcoursewarnings);
1459 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1461 // Try update course visibility without capability.
1462 $this->unassignUserCapability('moodle/course:visibility', $contextid, $roleid);
1463 $user = self::getDataGenerator()->create_user();
1464 $this->setUser($user);
1465 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1466 $course1['summaryformat'] = FORMAT_MOODLE;
1467 $courses = array($course1);
1468 $updatedcoursewarnings = core_course_external::update_courses($courses);
1469 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1470 $updatedcoursewarnings);
1471 $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1472 $course1['visible'] = 0;
1473 $courses = array($course1);
1474 $updatedcoursewarnings = core_course_external::update_courses($courses);
1475 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1476 $updatedcoursewarnings);
1477 $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1481 * Test delete course_module.
1483 public function test_delete_modules() {
1484 global $DB;
1486 // Ensure we reset the data after this test.
1487 $this->resetAfterTest(true);
1489 // Create a user.
1490 $user = self::getDataGenerator()->create_user();
1492 // Set the tests to run as the user.
1493 self::setUser($user);
1495 // Create a course to add the modules.
1496 $course = self::getDataGenerator()->create_course();
1498 // Create two test modules.
1499 $record = new stdClass();
1500 $record->course = $course->id;
1501 $module1 = self::getDataGenerator()->create_module('forum', $record);
1502 $module2 = self::getDataGenerator()->create_module('assign', $record);
1504 // Check the forum was correctly created.
1505 $this->assertEquals(1, $DB->count_records('forum', array('id' => $module1->id)));
1507 // Check the assignment was correctly created.
1508 $this->assertEquals(1, $DB->count_records('assign', array('id' => $module2->id)));
1510 // Check data exists in the course modules table.
1511 $this->assertEquals(2, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
1512 array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
1514 // Enrol the user in the course.
1515 $enrol = enrol_get_plugin('manual');
1516 $enrolinstances = enrol_get_instances($course->id, true);
1517 foreach ($enrolinstances as $courseenrolinstance) {
1518 if ($courseenrolinstance->enrol == "manual") {
1519 $instance = $courseenrolinstance;
1520 break;
1523 $enrol->enrol_user($instance, $user->id);
1525 // Assign capabilities to delete module 1.
1526 $modcontext = context_module::instance($module1->cmid);
1527 $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id);
1529 // Assign capabilities to delete module 2.
1530 $modcontext = context_module::instance($module2->cmid);
1531 $newrole = create_role('Role 2', 'role2', 'Role 2 description');
1532 $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id, $newrole);
1534 // Deleting these module instances.
1535 core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
1537 // Check the forum was deleted.
1538 $this->assertEquals(0, $DB->count_records('forum', array('id' => $module1->id)));
1540 // Check the assignment was deleted.
1541 $this->assertEquals(0, $DB->count_records('assign', array('id' => $module2->id)));
1543 // Check we retrieve no data in the course modules table.
1544 $this->assertEquals(0, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
1545 array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
1547 // Call with non-existent course module id and ensure exception thrown.
1548 try {
1549 core_course_external::delete_modules(array('1337'));
1550 $this->fail('Exception expected due to missing course module.');
1551 } catch (dml_missing_record_exception $e) {
1552 $this->assertEquals('invalidcoursemodule', $e->errorcode);
1555 // Create two modules.
1556 $module1 = self::getDataGenerator()->create_module('forum', $record);
1557 $module2 = self::getDataGenerator()->create_module('assign', $record);
1559 // Since these modules were recreated the user will not have capabilities
1560 // to delete them, ensure exception is thrown if they try.
1561 try {
1562 core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
1563 $this->fail('Exception expected due to missing capability.');
1564 } catch (moodle_exception $e) {
1565 $this->assertEquals('nopermissions', $e->errorcode);
1568 // Unenrol user from the course.
1569 $enrol->unenrol_user($instance, $user->id);
1571 // Try and delete modules from the course the user was unenrolled in, make sure exception thrown.
1572 try {
1573 core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
1574 $this->fail('Exception expected due to being unenrolled from the course.');
1575 } catch (moodle_exception $e) {
1576 $this->assertEquals('requireloginerror', $e->errorcode);
1581 * Test import_course into an empty course
1583 public function test_import_course_empty() {
1584 global $USER;
1586 $this->resetAfterTest(true);
1588 $course1 = self::getDataGenerator()->create_course();
1589 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id, 'name' => 'Forum test'));
1590 $page = $this->getDataGenerator()->create_module('page', array('course' => $course1->id, 'name' => 'Page test'));
1592 $course2 = self::getDataGenerator()->create_course();
1594 $course1cms = get_fast_modinfo($course1->id)->get_cms();
1595 $course2cms = get_fast_modinfo($course2->id)->get_cms();
1597 // Verify the state of the courses before we do the import.
1598 $this->assertCount(2, $course1cms);
1599 $this->assertEmpty($course2cms);
1601 // Setup the user to run the operation (ugly hack because validate_context() will
1602 // fail as the email is not set by $this->setAdminUser()).
1603 $this->setAdminUser();
1604 $USER->email = 'emailtopass@example.com';
1606 // Import from course1 to course2.
1607 core_course_external::import_course($course1->id, $course2->id, 0);
1609 // Verify that now we have two modules in both courses.
1610 $course1cms = get_fast_modinfo($course1->id)->get_cms();
1611 $course2cms = get_fast_modinfo($course2->id)->get_cms();
1612 $this->assertCount(2, $course1cms);
1613 $this->assertCount(2, $course2cms);
1615 // Verify that the names transfered across correctly.
1616 foreach ($course2cms as $cm) {
1617 if ($cm->modname === 'page') {
1618 $this->assertEquals($cm->name, $page->name);
1619 } else if ($cm->modname === 'forum') {
1620 $this->assertEquals($cm->name, $forum->name);
1621 } else {
1622 $this->fail('Unknown CM found.');
1628 * Test import_course into an filled course
1630 public function test_import_course_filled() {
1631 global $USER;
1633 $this->resetAfterTest(true);
1635 // Add forum and page to course1.
1636 $course1 = self::getDataGenerator()->create_course();
1637 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
1638 $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
1640 // Add quiz to course 2.
1641 $course2 = self::getDataGenerator()->create_course();
1642 $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
1644 $course1cms = get_fast_modinfo($course1->id)->get_cms();
1645 $course2cms = get_fast_modinfo($course2->id)->get_cms();
1647 // Verify the state of the courses before we do the import.
1648 $this->assertCount(2, $course1cms);
1649 $this->assertCount(1, $course2cms);
1651 // Setup the user to run the operation (ugly hack because validate_context() will
1652 // fail as the email is not set by $this->setAdminUser()).
1653 $this->setAdminUser();
1654 $USER->email = 'emailtopass@example.com';
1656 // Import from course1 to course2 without deleting content.
1657 core_course_external::import_course($course1->id, $course2->id, 0);
1659 $course2cms = get_fast_modinfo($course2->id)->get_cms();
1661 // Verify that now we have three modules in course2.
1662 $this->assertCount(3, $course2cms);
1664 // Verify that the names transfered across correctly.
1665 foreach ($course2cms as $cm) {
1666 if ($cm->modname === 'page') {
1667 $this->assertEquals($cm->name, $page->name);
1668 } else if ($cm->modname === 'forum') {
1669 $this->assertEquals($cm->name, $forum->name);
1670 } else if ($cm->modname === 'quiz') {
1671 $this->assertEquals($cm->name, $quiz->name);
1672 } else {
1673 $this->fail('Unknown CM found.');
1679 * Test import_course with only blocks set to backup
1681 public function test_import_course_blocksonly() {
1682 global $USER, $DB;
1684 $this->resetAfterTest(true);
1686 // Add forum and page to course1.
1687 $course1 = self::getDataGenerator()->create_course();
1688 $course1ctx = context_course::instance($course1->id);
1689 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
1690 $block = $this->getDataGenerator()->create_block('online_users', array('parentcontextid' => $course1ctx->id));
1692 $course2 = self::getDataGenerator()->create_course();
1693 $course2ctx = context_course::instance($course2->id);
1694 $initialblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
1695 $initialcmcount = count(get_fast_modinfo($course2->id)->get_cms());
1697 // Setup the user to run the operation (ugly hack because validate_context() will
1698 // fail as the email is not set by $this->setAdminUser()).
1699 $this->setAdminUser();
1700 $USER->email = 'emailtopass@example.com';
1702 // Import from course1 to course2 without deleting content, but excluding
1703 // activities.
1704 $options = array(
1705 array('name' => 'activities', 'value' => 0),
1706 array('name' => 'blocks', 'value' => 1),
1707 array('name' => 'filters', 'value' => 0),
1710 core_course_external::import_course($course1->id, $course2->id, 0, $options);
1712 $newcmcount = count(get_fast_modinfo($course2->id)->get_cms());
1713 $newblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
1714 // Check that course modules haven't changed, but that blocks have.
1715 $this->assertEquals($initialcmcount, $newcmcount);
1716 $this->assertEquals(($initialblockcount + 1), $newblockcount);
1720 * Test import_course into an filled course, deleting content.
1722 public function test_import_course_deletecontent() {
1723 global $USER;
1724 $this->resetAfterTest(true);
1726 // Add forum and page to course1.
1727 $course1 = self::getDataGenerator()->create_course();
1728 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
1729 $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
1731 // Add quiz to course 2.
1732 $course2 = self::getDataGenerator()->create_course();
1733 $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
1735 $course1cms = get_fast_modinfo($course1->id)->get_cms();
1736 $course2cms = get_fast_modinfo($course2->id)->get_cms();
1738 // Verify the state of the courses before we do the import.
1739 $this->assertCount(2, $course1cms);
1740 $this->assertCount(1, $course2cms);
1742 // Setup the user to run the operation (ugly hack because validate_context() will
1743 // fail as the email is not set by $this->setAdminUser()).
1744 $this->setAdminUser();
1745 $USER->email = 'emailtopass@example.com';
1747 // Import from course1 to course2, deleting content.
1748 core_course_external::import_course($course1->id, $course2->id, 1);
1750 $course2cms = get_fast_modinfo($course2->id)->get_cms();
1752 // Verify that now we have two modules in course2.
1753 $this->assertCount(2, $course2cms);
1755 // Verify that the course only contains the imported modules.
1756 foreach ($course2cms as $cm) {
1757 if ($cm->modname === 'page') {
1758 $this->assertEquals($cm->name, $page->name);
1759 } else if ($cm->modname === 'forum') {
1760 $this->assertEquals($cm->name, $forum->name);
1761 } else {
1762 $this->fail('Unknown CM found: '.$cm->name);
1768 * Ensure import_course handles incorrect deletecontent option correctly.
1770 public function test_import_course_invalid_deletecontent_option() {
1771 $this->resetAfterTest(true);
1773 $course1 = self::getDataGenerator()->create_course();
1774 $course2 = self::getDataGenerator()->create_course();
1776 $this->expectException('moodle_exception');
1777 $this->expectExceptionMessage(get_string('invalidextparam', 'webservice', -1));
1778 // Import from course1 to course2, with invalid option
1779 core_course_external::import_course($course1->id, $course2->id, -1);;
1783 * Test view_course function
1785 public function test_view_course() {
1787 $this->resetAfterTest();
1789 // Course without sections.
1790 $course = $this->getDataGenerator()->create_course(array('numsections' => 5), array('createsections' => true));
1791 $this->setAdminUser();
1793 // Redirect events to the sink, so we can recover them later.
1794 $sink = $this->redirectEvents();
1796 $result = core_course_external::view_course($course->id, 1);
1797 $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
1798 $events = $sink->get_events();
1799 $event = reset($events);
1801 // Check the event details are correct.
1802 $this->assertInstanceOf('\core\event\course_viewed', $event);
1803 $this->assertEquals(context_course::instance($course->id), $event->get_context());
1804 $this->assertEquals(1, $event->other['coursesectionnumber']);
1806 $result = core_course_external::view_course($course->id);
1807 $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
1808 $events = $sink->get_events();
1809 $event = array_pop($events);
1810 $sink->close();
1812 // Check the event details are correct.
1813 $this->assertInstanceOf('\core\event\course_viewed', $event);
1814 $this->assertEquals(context_course::instance($course->id), $event->get_context());
1815 $this->assertEmpty($event->other);
1820 * Test get_course_module
1822 public function test_get_course_module() {
1823 global $DB;
1825 $this->resetAfterTest(true);
1827 $this->setAdminUser();
1828 $course = self::getDataGenerator()->create_course();
1829 $record = array(
1830 'course' => $course->id,
1831 'name' => 'First Assignment'
1833 $options = array(
1834 'idnumber' => 'ABC',
1835 'visible' => 0
1837 // Hidden activity.
1838 $assign = self::getDataGenerator()->create_module('assign', $record, $options);
1840 $outcomescale = 'Distinction, Very Good, Good, Pass, Fail';
1842 // Insert a custom grade scale to be used by an outcome.
1843 $gradescale = new grade_scale();
1844 $gradescale->name = 'gettcoursemodulescale';
1845 $gradescale->courseid = $course->id;
1846 $gradescale->userid = 0;
1847 $gradescale->scale = $outcomescale;
1848 $gradescale->description = 'This scale is used to mark standard assignments.';
1849 $gradescale->insert();
1851 // Insert an outcome.
1852 $data = new stdClass();
1853 $data->courseid = $course->id;
1854 $data->fullname = 'Team work';
1855 $data->shortname = 'Team work';
1856 $data->scaleid = $gradescale->id;
1857 $outcome = new grade_outcome($data, false);
1858 $outcome->insert();
1860 $outcomegradeitem = new grade_item();
1861 $outcomegradeitem->itemname = $outcome->shortname;
1862 $outcomegradeitem->itemtype = 'mod';
1863 $outcomegradeitem->itemmodule = 'assign';
1864 $outcomegradeitem->iteminstance = $assign->id;
1865 $outcomegradeitem->outcomeid = $outcome->id;
1866 $outcomegradeitem->cmid = 0;
1867 $outcomegradeitem->courseid = $course->id;
1868 $outcomegradeitem->aggregationcoef = 0;
1869 $outcomegradeitem->itemnumber = 1; // The activity's original grade item will be 0.
1870 $outcomegradeitem->gradetype = GRADE_TYPE_SCALE;
1871 $outcomegradeitem->scaleid = $outcome->scaleid;
1872 $outcomegradeitem->insert();
1874 $assignmentgradeitem = grade_item::fetch(
1875 array(
1876 'itemtype' => 'mod',
1877 'itemmodule' => 'assign',
1878 'iteminstance' => $assign->id,
1879 'itemnumber' => 0,
1880 'courseid' => $course->id
1883 $outcomegradeitem->set_parent($assignmentgradeitem->categoryid);
1884 $outcomegradeitem->move_after_sortorder($assignmentgradeitem->sortorder);
1886 // Test admin user can see the complete hidden activity.
1887 $result = core_course_external::get_course_module($assign->cmid);
1888 $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
1890 $this->assertCount(0, $result['warnings']);
1891 // Test we retrieve all the fields.
1892 $this->assertCount(28, $result['cm']);
1893 $this->assertEquals($record['name'], $result['cm']['name']);
1894 $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
1895 $this->assertEquals(100, $result['cm']['grade']);
1896 $this->assertEquals(0.0, $result['cm']['gradepass']);
1897 $this->assertEquals('submissions', $result['cm']['advancedgrading'][0]['area']);
1898 $this->assertEmpty($result['cm']['advancedgrading'][0]['method']);
1899 $this->assertEquals($outcomescale, $result['cm']['outcomes'][0]['scale']);
1901 $student = $this->getDataGenerator()->create_user();
1902 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1904 self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1905 $this->setUser($student);
1907 // The user shouldn't be able to see the activity.
1908 try {
1909 core_course_external::get_course_module($assign->cmid);
1910 $this->fail('Exception expected due to invalid permissions.');
1911 } catch (moodle_exception $e) {
1912 $this->assertEquals('requireloginerror', $e->errorcode);
1915 // Make module visible.
1916 set_coursemodule_visible($assign->cmid, 1);
1918 // Test student user.
1919 $result = core_course_external::get_course_module($assign->cmid);
1920 $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
1922 $this->assertCount(0, $result['warnings']);
1923 // Test we retrieve only the few files we can see.
1924 $this->assertCount(11, $result['cm']);
1925 $this->assertEquals($assign->cmid, $result['cm']['id']);
1926 $this->assertEquals($course->id, $result['cm']['course']);
1927 $this->assertEquals('assign', $result['cm']['modname']);
1928 $this->assertEquals($assign->id, $result['cm']['instance']);
1933 * Test get_course_module_by_instance
1935 public function test_get_course_module_by_instance() {
1936 global $DB;
1938 $this->resetAfterTest(true);
1940 $this->setAdminUser();
1941 $course = self::getDataGenerator()->create_course();
1942 $record = array(
1943 'course' => $course->id,
1944 'name' => 'First quiz',
1945 'grade' => 90.00
1947 $options = array(
1948 'idnumber' => 'ABC',
1949 'visible' => 0
1951 // Hidden activity.
1952 $quiz = self::getDataGenerator()->create_module('quiz', $record, $options);
1954 // Test admin user can see the complete hidden activity.
1955 $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
1956 $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
1958 $this->assertCount(0, $result['warnings']);
1959 // Test we retrieve all the fields.
1960 $this->assertCount(26, $result['cm']);
1961 $this->assertEquals($record['name'], $result['cm']['name']);
1962 $this->assertEquals($record['grade'], $result['cm']['grade']);
1963 $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
1965 $student = $this->getDataGenerator()->create_user();
1966 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1968 self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1969 $this->setUser($student);
1971 // The user shouldn't be able to see the activity.
1972 try {
1973 core_course_external::get_course_module_by_instance('quiz', $quiz->id);
1974 $this->fail('Exception expected due to invalid permissions.');
1975 } catch (moodle_exception $e) {
1976 $this->assertEquals('requireloginerror', $e->errorcode);
1979 // Make module visible.
1980 set_coursemodule_visible($quiz->cmid, 1);
1982 // Test student user.
1983 $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
1984 $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
1986 $this->assertCount(0, $result['warnings']);
1987 // Test we retrieve only the few files we can see.
1988 $this->assertCount(11, $result['cm']);
1989 $this->assertEquals($quiz->cmid, $result['cm']['id']);
1990 $this->assertEquals($course->id, $result['cm']['course']);
1991 $this->assertEquals('quiz', $result['cm']['modname']);
1992 $this->assertEquals($quiz->id, $result['cm']['instance']);
1994 // Try with an invalid module name.
1995 try {
1996 core_course_external::get_course_module_by_instance('abc', $quiz->id);
1997 $this->fail('Exception expected due to invalid module name.');
1998 } catch (dml_read_exception $e) {
1999 $this->assertEquals('dmlreadexception', $e->errorcode);
2005 * Test get_activities_overview
2007 public function test_get_activities_overview() {
2008 global $USER;
2010 $this->resetAfterTest();
2011 $course1 = self::getDataGenerator()->create_course();
2012 $course2 = self::getDataGenerator()->create_course();
2014 // Create a viewer user.
2015 $viewer = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
2016 $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2017 $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2019 // Create two forums - one in each course.
2020 $record = new stdClass();
2021 $record->course = $course1->id;
2022 $forum1 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course1->id));
2023 $forum2 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course2->id));
2025 $this->setAdminUser();
2026 // A standard post in the forum.
2027 $record = new stdClass();
2028 $record->course = $course1->id;
2029 $record->userid = $USER->id;
2030 $record->forum = $forum1->id;
2031 $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2033 $this->setUser($viewer->id);
2034 $courses = array($course1->id , $course2->id);
2036 $result = core_course_external::get_activities_overview($courses);
2037 $this->assertDebuggingCalledCount(8);
2038 $result = external_api::clean_returnvalue(core_course_external::get_activities_overview_returns(), $result);
2040 // There should be one entry for course1, and no others.
2041 $this->assertCount(1, $result['courses']);
2042 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2043 // Check expected overview data for the module.
2044 $this->assertEquals('forum', $result['courses'][0]['overviews'][0]['module']);
2045 $this->assertContains('1 total unread', $result['courses'][0]['overviews'][0]['overviewtext']);
2049 * Test get_user_navigation_options
2051 public function test_get_user_navigation_options() {
2052 global $USER;
2054 $this->resetAfterTest();
2055 $course1 = self::getDataGenerator()->create_course();
2056 $course2 = self::getDataGenerator()->create_course();
2058 // Create a viewer user.
2059 $viewer = self::getDataGenerator()->create_user();
2060 $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2061 $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2063 $this->setUser($viewer->id);
2064 $courses = array($course1->id , $course2->id, SITEID);
2066 $result = core_course_external::get_user_navigation_options($courses);
2067 $result = external_api::clean_returnvalue(core_course_external::get_user_navigation_options_returns(), $result);
2069 $this->assertCount(0, $result['warnings']);
2070 $this->assertCount(3, $result['courses']);
2072 foreach ($result['courses'] as $course) {
2073 $navoptions = new stdClass;
2074 foreach ($course['options'] as $option) {
2075 $navoptions->{$option['name']} = $option['available'];
2077 $this->assertCount(9, $course['options']);
2078 if ($course['id'] == SITEID) {
2079 $this->assertTrue($navoptions->blogs);
2080 $this->assertFalse($navoptions->notes);
2081 $this->assertFalse($navoptions->participants);
2082 $this->assertTrue($navoptions->badges);
2083 $this->assertTrue($navoptions->tags);
2084 $this->assertFalse($navoptions->grades);
2085 $this->assertFalse($navoptions->search);
2086 $this->assertTrue($navoptions->calendar);
2087 $this->assertTrue($navoptions->competencies);
2088 } else {
2089 $this->assertTrue($navoptions->blogs);
2090 $this->assertFalse($navoptions->notes);
2091 $this->assertTrue($navoptions->participants);
2092 $this->assertTrue($navoptions->badges);
2093 $this->assertFalse($navoptions->tags);
2094 $this->assertTrue($navoptions->grades);
2095 $this->assertFalse($navoptions->search);
2096 $this->assertFalse($navoptions->calendar);
2097 $this->assertTrue($navoptions->competencies);
2103 * Test get_user_administration_options
2105 public function test_get_user_administration_options() {
2106 global $USER;
2108 $this->resetAfterTest();
2109 $course1 = self::getDataGenerator()->create_course();
2110 $course2 = self::getDataGenerator()->create_course();
2112 // Create a viewer user.
2113 $viewer = self::getDataGenerator()->create_user();
2114 $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2115 $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2117 $this->setUser($viewer->id);
2118 $courses = array($course1->id , $course2->id, SITEID);
2120 $result = core_course_external::get_user_administration_options($courses);
2121 $result = external_api::clean_returnvalue(core_course_external::get_user_administration_options_returns(), $result);
2123 $this->assertCount(0, $result['warnings']);
2124 $this->assertCount(3, $result['courses']);
2126 foreach ($result['courses'] as $course) {
2127 $adminoptions = new stdClass;
2128 foreach ($course['options'] as $option) {
2129 $adminoptions->{$option['name']} = $option['available'];
2131 if ($course['id'] == SITEID) {
2132 $this->assertCount(16, $course['options']);
2133 $this->assertFalse($adminoptions->update);
2134 $this->assertFalse($adminoptions->filters);
2135 $this->assertFalse($adminoptions->reports);
2136 $this->assertFalse($adminoptions->backup);
2137 $this->assertFalse($adminoptions->restore);
2138 $this->assertFalse($adminoptions->files);
2139 $this->assertFalse(!isset($adminoptions->tags));
2140 $this->assertFalse($adminoptions->gradebook);
2141 $this->assertFalse($adminoptions->outcomes);
2142 $this->assertFalse($adminoptions->badges);
2143 $this->assertFalse($adminoptions->import);
2144 $this->assertFalse($adminoptions->publish);
2145 $this->assertFalse($adminoptions->reset);
2146 $this->assertFalse($adminoptions->roles);
2147 $this->assertFalse($adminoptions->editcompletion);
2148 } else {
2149 $this->assertCount(15, $course['options']);
2150 $this->assertFalse($adminoptions->update);
2151 $this->assertFalse($adminoptions->filters);
2152 $this->assertFalse($adminoptions->reports);
2153 $this->assertFalse($adminoptions->backup);
2154 $this->assertFalse($adminoptions->restore);
2155 $this->assertFalse($adminoptions->files);
2156 $this->assertFalse($adminoptions->tags);
2157 $this->assertFalse($adminoptions->gradebook);
2158 $this->assertFalse($adminoptions->outcomes);
2159 $this->assertTrue($adminoptions->badges);
2160 $this->assertFalse($adminoptions->import);
2161 $this->assertFalse($adminoptions->publish);
2162 $this->assertFalse($adminoptions->reset);
2163 $this->assertFalse($adminoptions->roles);
2164 $this->assertFalse($adminoptions->editcompletion);
2170 * Test get_courses_by_fields
2172 public function test_get_courses_by_field() {
2173 global $DB;
2174 $this->resetAfterTest(true);
2176 $category1 = self::getDataGenerator()->create_category(array('name' => 'Cat 1'));
2177 $category2 = self::getDataGenerator()->create_category(array('parent' => $category1->id));
2178 $course1 = self::getDataGenerator()->create_course(
2179 array('category' => $category1->id, 'shortname' => 'c1', 'format' => 'topics'));
2180 $course2 = self::getDataGenerator()->create_course(array('visible' => 0, 'category' => $category2->id, 'idnumber' => 'i2'));
2182 $student1 = self::getDataGenerator()->create_user();
2183 $user1 = self::getDataGenerator()->create_user();
2184 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2185 self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id);
2186 self::getDataGenerator()->enrol_user($student1->id, $course2->id, $studentrole->id);
2188 self::setAdminUser();
2189 // As admins, we should be able to retrieve everything.
2190 $result = core_course_external::get_courses_by_field();
2191 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2192 $this->assertCount(3, $result['courses']);
2193 // Expect to receive all the fields.
2194 $this->assertCount(37, $result['courses'][0]);
2195 $this->assertCount(38, $result['courses'][1]); // One more field because is not the site course.
2196 $this->assertCount(38, $result['courses'][2]); // One more field because is not the site course.
2198 $result = core_course_external::get_courses_by_field('id', $course1->id);
2199 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2200 $this->assertCount(1, $result['courses']);
2201 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2202 // Expect to receive all the fields.
2203 $this->assertCount(38, $result['courses'][0]);
2204 // Check default values for course format topics.
2205 $this->assertCount(2, $result['courses'][0]['courseformatoptions']);
2206 foreach ($result['courses'][0]['courseformatoptions'] as $option) {
2207 if ($option['name'] == 'hiddensections') {
2208 $this->assertEquals(0, $option['value']);
2209 } else {
2210 $this->assertEquals('coursedisplay', $option['name']);
2211 $this->assertEquals(0, $option['value']);
2215 $result = core_course_external::get_courses_by_field('id', $course2->id);
2216 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2217 $this->assertCount(1, $result['courses']);
2218 $this->assertEquals($course2->id, $result['courses'][0]['id']);
2220 $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2221 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2222 $this->assertCount(2, $result['courses']);
2224 // Check default filters.
2225 $this->assertCount(3, $result['courses'][0]['filters']);
2226 $this->assertCount(3, $result['courses'][1]['filters']);
2228 $result = core_course_external::get_courses_by_field('category', $category1->id);
2229 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2230 $this->assertCount(1, $result['courses']);
2231 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2232 $this->assertEquals('Cat 1', $result['courses'][0]['categoryname']);
2234 $result = core_course_external::get_courses_by_field('shortname', 'c1');
2235 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2236 $this->assertCount(1, $result['courses']);
2237 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2239 $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2240 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2241 $this->assertCount(1, $result['courses']);
2242 $this->assertEquals($course2->id, $result['courses'][0]['id']);
2244 $result = core_course_external::get_courses_by_field('idnumber', 'x');
2245 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2246 $this->assertCount(0, $result['courses']);
2248 // Change filter value.
2249 filter_set_local_state('mediaplugin', context_course::instance($course1->id)->id, TEXTFILTER_OFF);
2251 self::setUser($student1);
2252 // All visible courses (including front page) for normal student.
2253 $result = core_course_external::get_courses_by_field();
2254 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2255 $this->assertCount(2, $result['courses']);
2256 $this->assertCount(30, $result['courses'][0]);
2257 $this->assertCount(31, $result['courses'][1]); // One field more (course format options), not present in site course.
2259 $result = core_course_external::get_courses_by_field('id', $course1->id);
2260 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2261 $this->assertCount(1, $result['courses']);
2262 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2263 // Expect to receive all the files that a student can see.
2264 $this->assertCount(31, $result['courses'][0]);
2266 // Check default filters.
2267 $filters = $result['courses'][0]['filters'];
2268 $this->assertCount(3, $filters);
2269 $found = false;
2270 foreach ($filters as $filter) {
2271 if ($filter['filter'] == 'mediaplugin' and $filter['localstate'] == TEXTFILTER_OFF) {
2272 $found = true;
2275 $this->assertTrue($found);
2277 // Course 2 is not visible.
2278 $result = core_course_external::get_courses_by_field('id', $course2->id);
2279 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2280 $this->assertCount(0, $result['courses']);
2282 $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2283 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2284 $this->assertCount(1, $result['courses']);
2286 $result = core_course_external::get_courses_by_field('category', $category1->id);
2287 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2288 $this->assertCount(1, $result['courses']);
2289 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2291 $result = core_course_external::get_courses_by_field('shortname', 'c1');
2292 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2293 $this->assertCount(1, $result['courses']);
2294 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2296 $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2297 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2298 $this->assertCount(0, $result['courses']);
2300 $result = core_course_external::get_courses_by_field('idnumber', 'x');
2301 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2302 $this->assertCount(0, $result['courses']);
2304 self::setUser($user1);
2305 // All visible courses (including front page) for authenticated user.
2306 $result = core_course_external::get_courses_by_field();
2307 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2308 $this->assertCount(2, $result['courses']);
2309 $this->assertCount(30, $result['courses'][0]); // Site course.
2310 $this->assertCount(13, $result['courses'][1]); // Only public information, not enrolled.
2312 $result = core_course_external::get_courses_by_field('id', $course1->id);
2313 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2314 $this->assertCount(1, $result['courses']);
2315 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2316 // Expect to receive all the files that a authenticated can see.
2317 $this->assertCount(13, $result['courses'][0]);
2319 // Course 2 is not visible.
2320 $result = core_course_external::get_courses_by_field('id', $course2->id);
2321 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2322 $this->assertCount(0, $result['courses']);
2324 $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2325 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2326 $this->assertCount(1, $result['courses']);
2328 $result = core_course_external::get_courses_by_field('category', $category1->id);
2329 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2330 $this->assertCount(1, $result['courses']);
2331 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2333 $result = core_course_external::get_courses_by_field('shortname', 'c1');
2334 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2335 $this->assertCount(1, $result['courses']);
2336 $this->assertEquals($course1->id, $result['courses'][0]['id']);
2338 $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2339 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2340 $this->assertCount(0, $result['courses']);
2342 $result = core_course_external::get_courses_by_field('idnumber', 'x');
2343 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2344 $this->assertCount(0, $result['courses']);
2347 public function test_get_courses_by_field_invalid_field() {
2348 $this->expectException('invalid_parameter_exception');
2349 $result = core_course_external::get_courses_by_field('zyx', 'x');
2352 public function test_get_courses_by_field_invalid_courses() {
2353 $result = core_course_external::get_courses_by_field('id', '-1');
2354 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2355 $this->assertCount(0, $result['courses']);
2359 * Test get_courses_by_field_invalid_theme_and_lang
2361 public function test_get_courses_by_field_invalid_theme_and_lang() {
2362 $this->resetAfterTest(true);
2363 $this->setAdminUser();
2365 $course = self::getDataGenerator()->create_course(array('theme' => 'kkt', 'lang' => 'kkl'));
2366 $result = core_course_external::get_courses_by_field('id', $course->id);
2367 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2368 $this->assertEmpty($result['courses']['0']['theme']);
2369 $this->assertEmpty($result['courses']['0']['lang']);
2373 public function test_check_updates() {
2374 global $DB;
2375 $this->resetAfterTest(true);
2376 $this->setAdminUser();
2378 // Create different types of activities.
2379 $course = self::getDataGenerator()->create_course();
2380 $tocreate = array('assign', 'book', 'choice', 'folder', 'forum', 'glossary', 'imscp', 'label', 'lti', 'page', 'quiz',
2381 'resource', 'scorm', 'survey', 'url', 'wiki');
2383 $modules = array();
2384 foreach ($tocreate as $modname) {
2385 $modules[$modname]['instance'] = $this->getDataGenerator()->create_module($modname, array('course' => $course->id));
2386 $modules[$modname]['cm'] = get_coursemodule_from_id(false, $modules[$modname]['instance']->cmid);
2387 $modules[$modname]['context'] = context_module::instance($modules[$modname]['instance']->cmid);
2390 $student = self::getDataGenerator()->create_user();
2391 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2392 self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
2393 $this->setUser($student);
2395 $since = time();
2396 $this->waitForSecond();
2397 $params = array();
2398 foreach ($modules as $modname => $data) {
2399 $params[$data['cm']->id] = array(
2400 'contextlevel' => 'module',
2401 'id' => $data['cm']->id,
2402 'since' => $since
2406 // Check there is nothing updated because modules are fresh new.
2407 $result = core_course_external::check_updates($course->id, $params);
2408 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
2409 $this->assertCount(0, $result['instances']);
2410 $this->assertCount(0, $result['warnings']);
2412 // Test with get_updates_since the same data.
2413 $result = core_course_external::get_updates_since($course->id, $since);
2414 $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
2415 $this->assertCount(0, $result['instances']);
2416 $this->assertCount(0, $result['warnings']);
2418 // Update a module after a second.
2419 $this->waitForSecond();
2420 set_coursemodule_name($modules['forum']['cm']->id, 'New forum name');
2422 $found = false;
2423 $result = core_course_external::check_updates($course->id, $params);
2424 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
2425 $this->assertCount(1, $result['instances']);
2426 $this->assertCount(0, $result['warnings']);
2427 foreach ($result['instances'] as $module) {
2428 foreach ($module['updates'] as $update) {
2429 if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
2430 $found = true;
2434 $this->assertTrue($found);
2436 // Test with get_updates_since the same data.
2437 $result = core_course_external::get_updates_since($course->id, $since);
2438 $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
2439 $this->assertCount(1, $result['instances']);
2440 $this->assertCount(0, $result['warnings']);
2441 $found = false;
2442 $this->assertCount(1, $result['instances']);
2443 $this->assertCount(0, $result['warnings']);
2444 foreach ($result['instances'] as $module) {
2445 foreach ($module['updates'] as $update) {
2446 if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
2447 $found = true;
2451 $this->assertTrue($found);
2453 // Do not retrieve the configuration field.
2454 $filter = array('files');
2455 $found = false;
2456 $result = core_course_external::check_updates($course->id, $params, $filter);
2457 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
2458 $this->assertCount(0, $result['instances']);
2459 $this->assertCount(0, $result['warnings']);
2460 $this->assertFalse($found);
2462 // Add invalid cmid.
2463 $params[] = array(
2464 'contextlevel' => 'module',
2465 'id' => -2,
2466 'since' => $since
2468 $result = core_course_external::check_updates($course->id, $params);
2469 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
2470 $this->assertCount(1, $result['warnings']);
2471 $this->assertEquals(-2, $result['warnings'][0]['itemid']);
2475 * Test cases for the get_enrolled_courses_by_timeline_classification test.
2477 public function get_get_enrolled_courses_by_timeline_classification_test_cases() {
2478 $now = time();
2479 $day = 86400;
2481 $coursedata = [
2483 'shortname' => 'apast',
2484 'startdate' => $now - ($day * 2),
2485 'enddate' => $now - $day
2488 'shortname' => 'bpast',
2489 'startdate' => $now - ($day * 2),
2490 'enddate' => $now - $day
2493 'shortname' => 'cpast',
2494 'startdate' => $now - ($day * 2),
2495 'enddate' => $now - $day
2498 'shortname' => 'dpast',
2499 'startdate' => $now - ($day * 2),
2500 'enddate' => $now - $day
2503 'shortname' => 'epast',
2504 'startdate' => $now - ($day * 2),
2505 'enddate' => $now - $day
2508 'shortname' => 'ainprogress',
2509 'startdate' => $now - $day,
2510 'enddate' => $now + $day
2513 'shortname' => 'binprogress',
2514 'startdate' => $now - $day,
2515 'enddate' => $now + $day
2518 'shortname' => 'cinprogress',
2519 'startdate' => $now - $day,
2520 'enddate' => $now + $day
2523 'shortname' => 'dinprogress',
2524 'startdate' => $now - $day,
2525 'enddate' => $now + $day
2528 'shortname' => 'einprogress',
2529 'startdate' => $now - $day,
2530 'enddate' => $now + $day
2533 'shortname' => 'afuture',
2534 'startdate' => $now + $day
2537 'shortname' => 'bfuture',
2538 'startdate' => $now + $day
2541 'shortname' => 'cfuture',
2542 'startdate' => $now + $day
2545 'shortname' => 'dfuture',
2546 'startdate' => $now + $day
2549 'shortname' => 'efuture',
2550 'startdate' => $now + $day
2554 // Raw enrolled courses result set should be returned in this order:
2555 // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
2556 // dfuture, dinprogress, dpast, efuture, einprogress, epast
2558 // By classification the offset values for each record should be:
2559 // COURSE_TIMELINE_FUTURE
2560 // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
2561 // COURSE_TIMELINE_INPROGRESS
2562 // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
2563 // COURSE_TIMELINE_PAST
2564 // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
2566 // NOTE: The offset applies to the unfiltered full set of courses before the classification
2567 // filtering is done.
2568 // E.g. In our example if an offset of 2 is given then it would mean the first
2569 // two courses (afuture, ainprogress) are ignored.
2570 return [
2571 'empty set' => [
2572 'coursedata' => [],
2573 'classification' => 'future',
2574 'limit' => 2,
2575 'offset' => 0,
2576 'expectedcourses' => [],
2577 'expectednextoffset' => 0
2579 // COURSE_TIMELINE_FUTURE.
2580 'future not limit no offset' => [
2581 'coursedata' => $coursedata,
2582 'classification' => 'future',
2583 'limit' => 0,
2584 'offset' => 0,
2585 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
2586 'expectednextoffset' => 15
2588 'future no offset' => [
2589 'coursedata' => $coursedata,
2590 'classification' => 'future',
2591 'limit' => 2,
2592 'offset' => 0,
2593 'expectedcourses' => ['afuture', 'bfuture'],
2594 'expectednextoffset' => 4
2596 'future offset' => [
2597 'coursedata' => $coursedata,
2598 'classification' => 'future',
2599 'limit' => 2,
2600 'offset' => 2,
2601 'expectedcourses' => ['bfuture', 'cfuture'],
2602 'expectednextoffset' => 7
2604 'future exact limit' => [
2605 'coursedata' => $coursedata,
2606 'classification' => 'future',
2607 'limit' => 5,
2608 'offset' => 0,
2609 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
2610 'expectednextoffset' => 13
2612 'future limit less results' => [
2613 'coursedata' => $coursedata,
2614 'classification' => 'future',
2615 'limit' => 10,
2616 'offset' => 0,
2617 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
2618 'expectednextoffset' => 15
2620 'future limit less results with offset' => [
2621 'coursedata' => $coursedata,
2622 'classification' => 'future',
2623 'limit' => 10,
2624 'offset' => 5,
2625 'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
2626 'expectednextoffset' => 15
2628 'all no limit or offset' => [
2629 'coursedata' => $coursedata,
2630 'classification' => 'all',
2631 'limit' => 0,
2632 'offset' => 0,
2633 'expectedcourses' => [
2634 'afuture',
2635 'ainprogress',
2636 'apast',
2637 'bfuture',
2638 'binprogress',
2639 'bpast',
2640 'cfuture',
2641 'cinprogress',
2642 'cpast',
2643 'dfuture',
2644 'dinprogress',
2645 'dpast',
2646 'efuture',
2647 'einprogress',
2648 'epast'
2650 'expectednextoffset' => 15
2652 'all limit no offset' => [
2653 'coursedata' => $coursedata,
2654 'classification' => 'all',
2655 'limit' => 5,
2656 'offset' => 0,
2657 'expectedcourses' => [
2658 'afuture',
2659 'ainprogress',
2660 'apast',
2661 'bfuture',
2662 'binprogress'
2664 'expectednextoffset' => 5
2666 'all limit and offset' => [
2667 'coursedata' => $coursedata,
2668 'classification' => 'all',
2669 'limit' => 5,
2670 'offset' => 5,
2671 'expectedcourses' => [
2672 'bpast',
2673 'cfuture',
2674 'cinprogress',
2675 'cpast',
2676 'dfuture'
2678 'expectednextoffset' => 10
2680 'all offset past result set' => [
2681 'coursedata' => $coursedata,
2682 'classification' => 'all',
2683 'limit' => 5,
2684 'offset' => 50,
2685 'expectedcourses' => [],
2686 'expectednextoffset' => 50
2692 * Test the get_enrolled_courses_by_timeline_classification function.
2694 * @dataProvider get_get_enrolled_courses_by_timeline_classification_test_cases()
2695 * @param array $coursedata Courses to create
2696 * @param string $classification Timeline classification
2697 * @param int $limit Maximum number of results
2698 * @param int $offset Offset the unfiltered courses result set by this amount
2699 * @param array $expectedcourses Expected courses in result
2700 * @param int $expectednextoffset Expected next offset value in result
2702 public function test_get_enrolled_courses_by_timeline_classification(
2703 $coursedata,
2704 $classification,
2705 $limit,
2706 $offset,
2707 $expectedcourses,
2708 $expectednextoffset
2710 $this->resetAfterTest();
2711 $generator = $this->getDataGenerator();
2713 $courses = array_map(function($coursedata) use ($generator) {
2714 return $generator->create_course($coursedata);
2715 }, $coursedata);
2717 $student = $generator->create_user();
2719 foreach ($courses as $course) {
2720 $generator->enrol_user($student->id, $course->id, 'student');
2723 $this->setUser($student);
2725 // NOTE: The offset applies to the unfiltered full set of courses before the classification
2726 // filtering is done.
2727 // E.g. In our example if an offset of 2 is given then it would mean the first
2728 // two courses (afuture, ainprogress) are ignored.
2729 $result = core_course_external::get_enrolled_courses_by_timeline_classification(
2730 $classification,
2731 $limit,
2732 $offset,
2733 'shortname ASC'
2735 $result = external_api::clean_returnvalue(
2736 core_course_external::get_enrolled_courses_by_timeline_classification_returns(),
2737 $result
2740 $actual = array_map(function($course) {
2741 return $course['shortname'];
2742 }, $result['courses']);
2744 $this->assertEquals($expectedcourses, $actual);
2745 $this->assertEquals($expectednextoffset, $result['nextoffset']);
2749 * Test the get_recent_courses function.
2751 public function test_get_recent_courses() {
2752 global $USER, $DB;
2754 $this->resetAfterTest();
2755 $generator = $this->getDataGenerator();
2757 set_config('hiddenuserfields', 'lastaccess');
2759 $courses = array();
2760 for ($i = 1; $i < 12; $i++) {
2761 $courses[] = $generator->create_course();
2764 $student = $generator->create_user();
2765 $teacher = $generator->create_user();
2767 foreach ($courses as $course) {
2768 $generator->enrol_user($student->id, $course->id, 'student');
2771 $generator->enrol_user($teacher->id, $courses[0]->id, 'teacher');
2773 $this->setUser($student);
2775 $result = core_course_external::get_recent_courses($USER->id);
2777 // No course accessed.
2778 $this->assertCount(0, $result);
2780 foreach ($courses as $course) {
2781 core_course_external::view_course($course->id);
2784 // Every course accessed.
2785 $result = core_course_external::get_recent_courses($USER->id);
2786 $this->assertCount( 11, $result);
2788 // Every course accessed, result limited to 10 courses.
2789 $result = core_course_external::get_recent_courses($USER->id, 10);
2790 $this->assertCount(10, $result);
2792 $guestcourse = $generator->create_course(
2793 (object)array('shortname' => 'guestcourse',
2794 'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
2795 'enrol_guest_password_0' => ''));
2796 core_course_external::view_course($guestcourse->id);
2798 // Every course accessed, even the not enrolled one.
2799 $result = core_course_external::get_recent_courses($USER->id);
2800 $this->assertCount(12, $result);
2802 // Offset 5, return 7 out of 12.
2803 $result = core_course_external::get_recent_courses($USER->id, 0, 5);
2804 $this->assertCount(7, $result);
2806 // Offset 5 and limit 3, return 3 out of 12.
2807 $result = core_course_external::get_recent_courses($USER->id, 3, 5);
2808 $this->assertCount(3, $result);
2810 // Sorted by course id ASC.
2811 $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id ASC');
2812 $this->assertEquals($courses[0]->id, array_shift($result)->id);
2814 // Sorted by course id DESC.
2815 $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id DESC');
2816 $this->assertEquals($guestcourse->id, array_shift($result)->id);
2818 // If last access is hidden, only get the courses where has viewhiddenuserfields capability.
2819 $this->setUser($teacher);
2820 $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
2821 $usercontext = context_user::instance($student->id);
2822 $this->assignUserCapability('moodle/user:viewdetails', $usercontext, $teacherroleid);
2824 // Sorted by course id DESC.
2825 $result = core_course_external::get_recent_courses($student->id);
2826 $this->assertCount(1, $result);
2827 $this->assertEquals($courses[0]->id, array_shift($result)->id);