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