Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | Ultima modificación | Ver Log |

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