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