Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | 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
 */
11 efrain 42
final class externallib_test extends externallib_advanced_testcase {
1 efrain 43
 
44
    /**
45
     * Tests set up
46
     */
47
    protected function setUp(): void {
48
        global $CFG;
49
        require_once($CFG->dirroot . '/course/externallib.php');
1441 ariadna 50
        parent::setUp();
1 efrain 51
    }
52
 
53
    /**
54
     * Test create_categories
55
     */
11 efrain 56
    public function test_create_categories(): void {
1 efrain 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
     */
11 efrain 134
    public function test_delete_categories(): void {
1 efrain 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
     */
11 efrain 177
    public function test_get_categories(): void {
1 efrain 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
     */
11 efrain 308
    public function test_update_categories(): void {
1 efrain 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
     */
11 efrain 371
    public function test_update_categories_moving(): void {
1 efrain 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
     */
11 efrain 409
    public function test_create_course_numsections(): void {
1 efrain 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
     */
11 efrain 443
    public function test_create_courses(): void {
1 efrain 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
     */
11 efrain 611
    public static function course_empty_field_provider(): array {
1 efrain 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
     */
11 efrain 680
    public function test_delete_courses(): void {
1 efrain 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
     */
11 efrain 740
    public function test_get_courses(): void {
1 efrain 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']));
1441 ariadna 895
 
896
        // Set the multilang filter to apply to strings + reset filer caches.
897
        filter_set_global_state('multilang', TEXTFILTER_ON);
898
        filter_set_applies_to_strings('multilang', true);
899
        \filter_manager::reset_caches();
900
 
901
        // Let's create a custom field (number), and test the placeholders/multilang display.
902
        /** @var core_customfield_generator $cfgenerator */
903
        $cfgenerator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
904
        $numberfieldata = [
905
            'categoryid' => $fieldcategory->get('id'),
906
            'name' => 'Price',
907
            'shortname' => 'price',
908
            'type' => 'number',
909
            'configdata' => [
910
                'display' => '{value}',
911
                'decimalplaces' => 2,
912
            ],
913
        ];
914
 
915
        // Create a number custom field with default display template.
916
        $numberfield = $cfgenerator->create_field($numberfieldata);
917
        $cfgenerator->add_instance_data($numberfield, $newcourse->id, 15);
918
 
919
        // Create a number custom field with multilang display template.
920
        $numberfieldata['name'] = 'Price (multilang)';
921
        $numberfieldata['shortname'] = 'pricemultilang';
922
        $numberfieldata['configdata']['display'] = '<span lang="en" class="multilang">$ {value}</span>'
923
            . '<span lang="es" class="multilang">€ {value}</span>';
924
        $numberfield1 = $cfgenerator->create_field($numberfieldata);
925
        $cfgenerator->add_instance_data($numberfield1, $newcourse->id, 20);
926
 
927
        $courses = external_api::clean_returnvalue(
928
            core_course_external::get_courses_returns(),
929
            core_course_external::get_courses(['ids' => [$newcourse->id]])
930
        );
931
 
932
        $course = reset($courses);
933
        $this->assertCount(3, $course['customfields']);
934
 
935
        // Assert the received number custom fields display placeholders correctly with multilang filter when applied.
936
        $this->assertEquals('15.00', $course['customfields'][1]['value']);
937
        $this->assertEquals('$ 20.00', $course['customfields'][2]['value']);
1 efrain 938
    }
939
 
940
    /**
941
     * Test get_courses without capability
942
     */
11 efrain 943
    public function test_get_courses_without_capability(): void {
1 efrain 944
        $this->resetAfterTest(true);
945
 
946
        $course1 = $this->getDataGenerator()->create_course();
947
        $this->setUser($this->getDataGenerator()->create_user());
948
 
949
        // No permissions are required to get the site course.
950
        $courses = core_course_external::get_courses(array('ids' => [SITEID]));
951
        $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
952
 
953
        $this->assertEquals(1, count($courses));
954
        $this->assertEquals('PHPUnit test site', $courses[0]['fullname']);
955
        $this->assertEquals('site', $courses[0]['format']);
956
 
957
        // Requesting course without being enrolled or capability to view it will throw an exception.
958
        try {
959
            core_course_external::get_courses(array('ids' => [$course1->id]));
960
            $this->fail('Exception expected');
961
        } catch (moodle_exception $e) {
962
            $this->assertEquals(1, preg_match('/Course or activity not accessible. \(Not enrolled\)/', $e->getMessage()));
963
        }
964
    }
965
 
966
    /**
967
     * Test search_courses
968
     */
11 efrain 969
    public function test_search_courses(): void {
1 efrain 970
 
971
        global $DB;
972
 
973
        $this->resetAfterTest(true);
974
        $this->setAdminUser();
975
        $generatedcourses = array();
976
        $coursedata1['fullname'] = 'FIRST COURSE';
977
        $course1  = self::getDataGenerator()->create_course($coursedata1);
978
 
979
        $page = new moodle_page();
980
        $page->set_course($course1);
981
        $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
982
 
983
        $coursedata2['fullname'] = 'SECOND COURSE';
984
        $course2  = self::getDataGenerator()->create_course($coursedata2);
985
 
986
        $page = new moodle_page();
987
        $page->set_course($course2);
988
        $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
989
 
990
        // Search by name.
991
        $results = core_course_external::search_courses('search', 'FIRST');
992
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
993
        $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
994
        $this->assertCount(1, $results['courses']);
995
 
996
        // Create the forum.
997
        $record = new stdClass();
998
        $record->introformat = FORMAT_HTML;
999
        $record->course = $course2->id;
1000
        // Set Aggregate type = Average of ratings.
1001
        $forum = self::getDataGenerator()->create_module('forum', $record);
1002
 
1003
        // Search by module.
1004
        $results = core_course_external::search_courses('modulelist', 'forum');
1005
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
1006
        $this->assertEquals(1, $results['total']);
1007
 
1008
        // Enable coursetag option.
1009
        set_config('block_tags_showcoursetags', true);
1010
        // Add tag 'TAG-LABEL ON SECOND COURSE' to Course2.
1011
        core_tag_tag::set_item_tags('core', 'course', $course2->id, context_course::instance($course2->id),
1012
                array('TAG-LABEL ON SECOND COURSE'));
1013
        $taginstance = $DB->get_record('tag_instance',
1014
                array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST);
1015
 
1016
        // Search by tagid.
1017
        $results = core_course_external::search_courses('tagid', $taginstance->tagid);
1018
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
1019
        $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
1020
 
1021
        // Search by block (use news_items default block).
1022
        $blockid = $DB->get_field('block', 'id', array('name' => 'news_items'));
1023
        $results = core_course_external::search_courses('blocklist', $blockid);
1024
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
1025
        $this->assertEquals(2, $results['total']);
1026
 
1027
        // Now as a normal user.
1028
        $user = self::getDataGenerator()->create_user();
1029
 
1030
        // Add a 3rd, hidden, course we shouldn't see, even when enrolled as student.
1031
        $coursedata3['fullname'] = 'HIDDEN COURSE';
1032
        $coursedata3['visible'] = 0;
1033
        $course3  = self::getDataGenerator()->create_course($coursedata3);
1034
        $this->getDataGenerator()->enrol_user($user->id, $course3->id, 'student');
1035
 
1036
        $this->getDataGenerator()->enrol_user($user->id, $course2->id, 'student');
1037
        $this->setUser($user);
1038
 
1039
        $results = core_course_external::search_courses('search', 'FIRST');
1040
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
1041
        $this->assertCount(1, $results['courses']);
1042
        $this->assertEquals(1, $results['total']);
1043
        $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
1044
 
1045
        // Check that we can see all courses without the limit to enrolled setting.
1046
        $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 0);
1047
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
1048
        $this->assertCount(2, $results['courses']);
1049
        $this->assertEquals(2, $results['total']);
1050
 
1051
        // Check that we only see our enrolled course when limiting.
1052
        $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 1);
1053
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
1054
        $this->assertCount(1, $results['courses']);
1055
        $this->assertEquals(1, $results['total']);
1056
        $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
1057
 
1058
        // Search by block (use news_items default block). Should fail (only admins allowed).
1059
        $this->expectException('required_capability_exception');
1060
        $results = core_course_external::search_courses('blocklist', $blockid);
1061
    }
1062
 
1063
    /**
1064
     * Test searching for courses returns custom field data
1065
     */
1066
    public function test_search_courses_customfields(): void {
1067
        $this->resetAfterTest();
1068
        $this->setAdminUser();
1069
 
1070
        $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
1071
        $datefield = $this->getDataGenerator()->create_custom_field([
1072
            'categoryid' => $fieldcategory->get('id'),
1073
            'shortname' => 'mydate',
1074
            'name' => 'My date',
1075
            'type' => 'date',
1076
        ]);
1077
 
1078
        $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
1079
            [
1080
                'shortname' => $datefield->get('shortname'),
1081
                'value' => 1580389200, // 30/01/2020 13:00 GMT.
1082
            ],
1083
        ]]);
1084
 
1085
        $result = external_api::clean_returnvalue(
1086
            core_course_external::search_courses_returns(),
1087
            core_course_external::search_courses('search', $newcourse->shortname)
1088
        );
1089
 
1090
        $this->assertCount(1, $result['courses']);
1091
        $course = reset($result['courses']);
1092
 
1093
        $this->assertArrayHasKey('customfields', $course);
1094
        $this->assertCount(1, $course['customfields']);
1095
 
1096
        // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
1097
        $this->assertEquals([
1098
            'name' => $datefield->get('name'),
1099
            'shortname' => $datefield->get('shortname'),
1100
            'type' => $datefield->get('type'),
1101
            'value' => userdate(1580389200),
1102
            'valueraw' => 1580389200,
1103
        ], reset($course['customfields']));
1104
    }
1105
 
1106
    /**
1107
     * Create a course with contents
1108
     * @return array A list with the course object and course modules objects
1109
     */
1110
    private function prepare_get_course_contents_test() {
1111
        global $DB, $CFG;
1112
 
1113
        $CFG->allowstealth = 1; // Allow stealth activities.
1114
        $CFG->enablecompletion = true;
1115
        // Course with 4 sections (apart from the main section), with completion and not displaying hidden sections.
1116
        $course  = self::getDataGenerator()->create_course(['numsections' => 4, 'enablecompletion' => 1, 'hiddensections' => 1]);
1117
 
1118
        $forumdescription = 'This is the forum description';
1119
        $forum = $this->getDataGenerator()->create_module('forum',
1120
            array('course' => $course->id, 'intro' => $forumdescription, 'trackingtype' => 2),
1121
            array('showdescription' => true, 'completion' => COMPLETION_TRACKING_MANUAL));
1122
        $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1123
        // Add discussions to the tracking forced forum.
1124
        $record = new stdClass();
1125
        $record->course = $course->id;
1126
        $record->userid = 0;
1127
        $record->forum = $forum->id;
1128
        $discussionforce = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1129
        $data = $this->getDataGenerator()->create_module('data',
1130
            array('assessed' => 1, 'scale' => 100, 'course' => $course->id, 'completion' => 2, 'completionentries' => 3));
1131
        $datacm = get_coursemodule_from_instance('data', $data->id);
1132
        $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
1133
        $pagecm = get_coursemodule_from_instance('page', $page->id);
1134
        // This is an stealth page (set by visibleoncoursepage).
1135
        $pagestealth = $this->getDataGenerator()->create_module('page', array('course' => $course->id, 'visibleoncoursepage' => 0));
1136
        $labeldescription = 'This is a very long label to test if more than 50 characters are returned.
1137
                So bla bla bla bla <b>bold bold bold</b> bla bla bla bla.';
1138
        $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
1139
            'intro' => $labeldescription, 'completion' => COMPLETION_TRACKING_MANUAL));
1140
        $labelcm = get_coursemodule_from_instance('label', $label->id);
1141
        $tomorrow = time() + DAYSECS;
1142
        // Module with availability restrictions not met.
1143
        $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '},'
1144
                .'{"type":"completion","cm":' . $label->cmid .',"e":1}],"showc":[true,true]}';
1145
        $url = $this->getDataGenerator()->create_module('url',
1146
            array('course' => $course->id, 'name' => 'URL: % & $ ../', 'section' => 2, 'display' => RESOURCELIB_DISPLAY_POPUP,
1147
                'popupwidth' => 100, 'popupheight' => 100),
1148
            array('availability' => $availability));
1149
        $urlcm = get_coursemodule_from_instance('url', $url->id);
1150
        // Module for the last section.
1151
        $this->getDataGenerator()->create_module('url',
1152
            array('course' => $course->id, 'name' => 'URL for last section', 'section' => 3));
1153
        // Module for section 1 with availability restrictions met.
1154
        $yesterday = time() - DAYSECS;
1155
        $this->getDataGenerator()->create_module('url',
1156
            array('course' => $course->id, 'name' => 'URL restrictions met', 'section' => 1),
1157
            array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":'. $yesterday .'}],"showc":[true]}'));
1158
 
1159
        // Set the required capabilities by the external function.
1160
        $context = context_course::instance($course->id);
1161
        $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
1162
        $this->assignUserCapability('moodle/course:update', $context->id, $roleid);
1163
        $this->assignUserCapability('mod/data:view', $context->id, $roleid);
1164
 
1165
        $conditions = array('course' => $course->id, 'section' => 2);
1166
        $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
1167
 
1168
        // Add date availability condition not met for section 3.
1169
        $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}';
1170
        $DB->set_field('course_sections', 'availability', $availability,
1171
                array('course' => $course->id, 'section' => 3));
1172
 
1173
        // Create resource for last section.
1174
        $pageinhiddensection = $this->getDataGenerator()->create_module('page',
1175
            array('course' => $course->id, 'name' => 'Page in hidden section', 'section' => 4));
1176
        // Set not visible last section.
1177
        $DB->set_field('course_sections', 'visible', 0,
1178
                array('course' => $course->id, 'section' => 4));
1179
 
1180
        $forumcompleteauto = $this->getDataGenerator()->create_module('forum',
1181
            array('course' => $course->id, 'intro' => 'forum completion tracking auto', 'trackingtype' => 2),
1182
            array('showdescription' => true, 'completionview' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC));
1183
        $forumcompleteautocm = get_coursemodule_from_id('forum', $forumcompleteauto->cmid);
1184
        $sectionrecord = $DB->get_record('course_sections', $conditions);
1185
        // Invalidate the section cache by given section number.
1186
        course_modinfo::purge_course_section_cache_by_number($sectionrecord->course, $sectionrecord->section);
1187
        rebuild_course_cache($course->id, true, true);
1188
 
1189
        return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm);
1190
    }
1191
 
1192
    /**
1193
     * Test get_course_contents
1194
     */
11 efrain 1195
    public function test_get_course_contents(): void {
1 efrain 1196
        global $CFG;
1197
        $this->resetAfterTest(true);
1198
 
1199
        $CFG->forum_allowforcedreadtracking = 1;
1200
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1201
 
1202
        // Create a resource with all the appearance options enabled. By default it's a text file and will be added to section 1.
1203
        $record = (object) [
1204
            'course' => $course->id,
1205
            'showsize' => 1,
1206
            'showtype' => 1,
1207
            'showdate' => 1,
1208
        ];
1209
        $resource = self::getDataGenerator()->create_module('resource', $record);
1210
        $h5pactivity = self::getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
1211
 
1212
        // We first run the test as admin.
1213
        $this->setAdminUser();
1214
        $sections = core_course_external::get_course_contents($course->id, array());
1215
        // We need to execute the return values cleaning process to simulate the web service server.
1216
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1217
 
1441 ariadna 1218
        $this->assertEmpty($sections[0]['component']);
1219
        $this->assertEmpty($sections[0]['itemid']);
1220
 
1 efrain 1221
        $modinfo = get_fast_modinfo($course);
1222
        $testexecuted = 0;
1223
        foreach ($sections[0]['modules'] as $module) {
1224
            if ($module['id'] == $forumcm->id and $module['modname'] == 'forum') {
1225
                $cm = $modinfo->cms[$forumcm->id];
1226
                $formattedtext = format_text($cm->content, FORMAT_HTML,
1227
                    array('noclean' => true, 'para' => false, 'filter' => false));
1228
                $this->assertEquals($formattedtext, $module['description']);
1229
                $this->assertEquals($forumcm->instance, $module['instance']);
1230
                $this->assertEquals(context_module::instance($forumcm->id)->id, $module['contextid']);
1231
                $this->assertFalse($module['noviewlink']);
1232
                $this->assertNotEmpty($module['description']);  // Module showdescription is on.
1233
                // Afterlink for forums has been removed; it has been moved to the new activity badge content.
1234
                $this->assertEmpty($module['afterlink']);
1235
                $this->assertEquals('1 unread post', $module['activitybadge']['badgecontent']);
1236
                $this->assertEquals('bg-dark text-white', $module['activitybadge']['badgestyle']);
1237
                $this->assertEquals(
1238
                    plugin_supports(
1239
                        'mod',
1240
                        'forum',
1241
                        FEATURE_MOD_PURPOSE,
1242
                        MOD_PURPOSE_OTHER
1243
                    ), $module['purpose']
1244
                );
1245
                $this->assertFalse($module['branded']);
1246
                $this->assertStringContainsString('trackingtype', $module['customdata']);   // The customdata is JSON encoded.
1247
                $testexecuted = $testexecuted + 2;
1248
            } else if ($module['id'] == $labelcm->id and $module['modname'] == 'label') {
1249
                $cm = $modinfo->cms[$labelcm->id];
1250
                $formattedtext = format_text($cm->content, FORMAT_HTML,
1251
                    array('noclean' => true, 'para' => false, 'filter' => false));
1252
                $this->assertEquals($formattedtext, $module['description']);
1253
                $this->assertEquals($labelcm->instance, $module['instance']);
1254
                $this->assertEquals(context_module::instance($labelcm->id)->id, $module['contextid']);
1255
                $this->assertTrue($module['noviewlink']);
1256
                $this->assertNotEmpty($module['description']);  // Label always prints the description.
1257
                $this->assertEquals(
1258
                    plugin_supports(
1259
                        'mod',
1260
                        'label',
1261
                        FEATURE_MOD_PURPOSE,
1262
                        MOD_PURPOSE_OTHER
1263
                    ), $module['purpose']
1264
                );
1265
                $this->assertFalse($module['branded']);
1266
                $testexecuted = $testexecuted + 1;
1267
            } else if ($module['id'] == $datacm->id and $module['modname'] == 'data') {
1268
                $this->assertStringContainsString('customcompletionrules', $module['customdata']);
1269
                $this->assertFalse($module['noviewlink']);
1270
                $this->assertArrayNotHasKey('description', $module);
1271
                $this->assertEquals(
1272
                    plugin_supports(
1273
                        'mod',
1274
                        'data',
1275
                        FEATURE_MOD_PURPOSE,
1276
                        MOD_PURPOSE_OTHER
1277
                    ), $module['purpose']
1278
                );
1279
                $this->assertFalse($module['branded']);
1280
                $testexecuted = $testexecuted + 1;
1281
            } else if ($module['instance'] == $resource->id && $module['modname'] == 'resource') {
1282
                // Resources have both, afterlink for the size and the update date and activitybadge for the file type.
1283
                $this->assertStringContainsString('32 bytes', $module['afterlink']);
1284
                $this->assertEquals('TXT', $module['activitybadge']['badgecontent']);
1285
                $this->assertEquals('badge-none', $module['activitybadge']['badgestyle']);
1286
                $this->assertEquals(
1287
                    plugin_supports(
1288
                        'mod',
1289
                        'resource',
1290
                        FEATURE_MOD_PURPOSE,
1291
                        MOD_PURPOSE_OTHER
1292
                    ), $module['purpose']
1293
                );
1294
                $this->assertFalse($module['branded']);
1295
                $testexecuted = $testexecuted + 1;
1296
            } else if ($module['instance'] == $h5pactivity->id && $module['modname'] == 'h5pactivity') {
1297
                $this->assertEquals(
1298
                    plugin_supports(
1299
                        'mod',
1300
                        'h5pactivity',
1301
                        FEATURE_MOD_PURPOSE,
1302
                        MOD_PURPOSE_OTHER
1303
                    ), $module['purpose']
1304
                );
1305
                $this->assertTrue($module['branded']);
1306
                $testexecuted = $testexecuted + 1;
1307
            }
1308
        }
1309
        foreach ($sections[2]['modules'] as $module) {
1310
            if ($module['id'] == $urlcm->id and $module['modname'] == 'url') {
1311
                $this->assertStringContainsString('width=100,height=100', $module['onclick']);
1312
                $testexecuted = $testexecuted + 1;
1313
            }
1314
        }
1315
 
1316
        $CFG->forum_allowforcedreadtracking = 0;    // Recover original value.
1317
        forum_tp_count_forum_unread_posts($forumcm, $course, true);    // Reset static cache for further tests.
1318
 
1319
        $this->assertEquals(7, $testexecuted);
1320
        $this->assertEquals(0, $sections[0]['section']);
1321
 
1322
        $this->assertCount(8, $sections[0]['modules']);
1323
        $this->assertCount(1, $sections[1]['modules']);
1324
        $this->assertCount(1, $sections[2]['modules']);
1325
        $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions.
1326
        $this->assertCount(1, $sections[4]['modules']); // One module for the hidden section with a visible activity.
1327
        $this->assertNotEmpty($sections[3]['availabilityinfo']);
1328
        $this->assertEquals(1, $sections[1]['section']);
1329
        $this->assertEquals(2, $sections[2]['section']);
1330
        $this->assertEquals(3, $sections[3]['section']);
1331
        $this->assertEquals(4, $sections[4]['section']);
1332
        $this->assertStringContainsString('<iframe', $sections[2]['summary']);
1333
        $this->assertStringContainsString('</iframe>', $sections[2]['summary']);
1334
        $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']);
1335
        try {
1336
            $sections = core_course_external::get_course_contents($course->id,
1337
                                                                    array(array("name" => "invalid", "value" => 1)));
1338
            $this->fail('Exception expected due to invalid option.');
1339
        } catch (moodle_exception $e) {
1340
            $this->assertEquals('errorinvalidparam', $e->errorcode);
1341
        }
1342
    }
1343
 
1344
 
1345
    /**
1346
     * Test get_course_contents as student
1347
     */
11 efrain 1348
    public function test_get_course_contents_student(): void {
1 efrain 1349
        global $DB;
1350
        $this->resetAfterTest(true);
1351
 
1352
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1353
 
1354
        $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
1355
        $user = self::getDataGenerator()->create_user();
1356
        self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
1357
        $this->setUser($user);
1358
 
1359
        $sections = core_course_external::get_course_contents($course->id, array());
1360
        // We need to execute the return values cleaning process to simulate the web service server.
1361
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1362
 
1363
        $this->assertCount(4, $sections); // Nothing for the not visible section.
1364
        $this->assertCount(6, $sections[0]['modules']);
1365
        $this->assertCount(1, $sections[1]['modules']);
1366
        $this->assertCount(1, $sections[2]['modules']);
1367
        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1368
 
1369
        $this->assertNotEmpty($sections[3]['availabilityinfo']);
1370
        $this->assertEquals(1, $sections[1]['section']);
1371
        $this->assertEquals(2, $sections[2]['section']);
1372
        $this->assertEquals(3, $sections[3]['section']);
1373
        // The module with the availability restriction met is returning contents.
1374
        $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
1375
        // The module with the availability restriction not met is not returning contents.
1376
        $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
1377
 
1378
        // Now include flag for returning stealth information (fake section).
1379
        $sections = core_course_external::get_course_contents($course->id,
1380
            array(array("name" => "includestealthmodules", "value" => 1)));
1381
        // We need to execute the return values cleaning process to simulate the web service server.
1382
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1383
 
1384
        $this->assertCount(5, $sections); // Include fake section with stealth activities.
1385
        $this->assertCount(6, $sections[0]['modules']);
1386
        $this->assertCount(1, $sections[1]['modules']);
1387
        $this->assertCount(1, $sections[2]['modules']);
1388
        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1389
        $this->assertCount(1, $sections[4]['modules']); // One stealth module.
1390
        $this->assertEquals(-1, $sections[4]['id']);
1391
    }
1392
 
1393
    /**
1394
     * Test get_course_contents excluding modules
1395
     */
11 efrain 1396
    public function test_get_course_contents_excluding_modules(): void {
1 efrain 1397
        $this->resetAfterTest(true);
1398
 
1399
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1400
 
1401
        // Test exclude modules.
1402
        $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludemodules", "value" => 1)));
1403
 
1404
        // We need to execute the return values cleaning process to simulate the web service server.
1405
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1406
 
1407
        $this->assertEmpty($sections[0]['modules']);
1408
        $this->assertEmpty($sections[1]['modules']);
1409
    }
1410
 
1411
    /**
1412
     * Test get_course_contents excluding contents
1413
     */
11 efrain 1414
    public function test_get_course_contents_excluding_contents(): void {
1 efrain 1415
        $this->resetAfterTest(true);
1416
 
1417
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1418
 
1419
        // Test exclude modules.
1420
        $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludecontents", "value" => 1)));
1421
 
1422
        // We need to execute the return values cleaning process to simulate the web service server.
1423
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1424
 
1425
        foreach ($sections as $section) {
1426
            foreach ($section['modules'] as $module) {
1427
                // Only resources return contents.
1428
                if (isset($module['contents'])) {
1429
                    $this->assertEmpty($module['contents']);
1430
                }
1431
            }
1432
        }
1433
    }
1434
 
1435
    /**
1436
     * Test get_course_contents filtering by section number
1437
     */
11 efrain 1438
    public function test_get_course_contents_section_number(): void {
1 efrain 1439
        $this->resetAfterTest(true);
1440
 
1441
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1442
 
1443
        // Test exclude modules.
1444
        $sections = core_course_external::get_course_contents($course->id, array(array("name" => "sectionnumber", "value" => 0)));
1445
 
1446
        // We need to execute the return values cleaning process to simulate the web service server.
1447
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1448
 
1449
        $this->assertCount(1, $sections);
1450
        $this->assertCount(6, $sections[0]['modules']);
1451
    }
1452
 
1453
    /**
1454
     * Test get_course_contents filtering by cmid
1455
     */
11 efrain 1456
    public function test_get_course_contents_cmid(): void {
1 efrain 1457
        $this->resetAfterTest(true);
1458
 
1459
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1460
 
1461
        // Test exclude modules.
1462
        $sections = core_course_external::get_course_contents($course->id, array(array("name" => "cmid", "value" => $forumcm->id)));
1463
 
1464
        // We need to execute the return values cleaning process to simulate the web service server.
1465
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1466
 
1467
        $this->assertCount(4, $sections);
1468
        $this->assertCount(1, $sections[0]['modules']);
1469
        $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1470
    }
1471
 
1472
 
1473
    /**
1474
     * Test get_course_contents filtering by cmid and section
1475
     */
11 efrain 1476
    public function test_get_course_contents_section_cmid(): void {
1 efrain 1477
        $this->resetAfterTest(true);
1478
 
1479
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1480
 
1481
        // Test exclude modules.
1482
        $sections = core_course_external::get_course_contents($course->id, array(
1483
                                                                        array("name" => "cmid", "value" => $forumcm->id),
1484
                                                                        array("name" => "sectionnumber", "value" => 0)
1485
                                                                        ));
1486
 
1487
        // We need to execute the return values cleaning process to simulate the web service server.
1488
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1489
 
1490
        $this->assertCount(1, $sections);
1491
        $this->assertCount(1, $sections[0]['modules']);
1492
        $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1493
    }
1494
 
1495
    /**
1496
     * Test get_course_contents filtering by modname
1497
     */
11 efrain 1498
    public function test_get_course_contents_modname(): void {
1 efrain 1499
        $this->resetAfterTest(true);
1500
 
1501
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1502
 
1503
        // Test exclude modules.
1504
        $sections = core_course_external::get_course_contents($course->id, array(array("name" => "modname", "value" => "forum")));
1505
 
1506
        // We need to execute the return values cleaning process to simulate the web service server.
1507
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1508
 
1509
        $this->assertCount(4, $sections);
1510
        $this->assertCount(2, $sections[0]['modules']);
1511
        $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1512
    }
1513
 
1514
    /**
1515
     * Test get_course_contents filtering by modname
1516
     */
11 efrain 1517
    public function test_get_course_contents_modid(): void {
1 efrain 1518
        $this->resetAfterTest(true);
1519
 
1520
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1521
 
1522
        // Test exclude modules.
1523
        $sections = core_course_external::get_course_contents($course->id, array(
1524
                                                                            array("name" => "modname", "value" => "page"),
1525
                                                                            array("name" => "modid", "value" => $pagecm->instance),
1526
                                                                            ));
1527
 
1528
        // We need to execute the return values cleaning process to simulate the web service server.
1529
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1530
 
1531
        $this->assertCount(4, $sections);
1532
        $this->assertCount(1, $sections[0]['modules']);
1533
        $this->assertEquals("page", $sections[0]['modules'][0]["modname"]);
1534
        $this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]);
1535
    }
1536
 
1537
    /**
1538
     * Test get_course_contents returns downloadcontent value.
1539
     */
11 efrain 1540
    public function test_get_course_contents_downloadcontent(): void {
1 efrain 1541
        $this->resetAfterTest();
1542
 
1543
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1544
 
1545
        // Test exclude modules.
1546
        $sections = core_course_external::get_course_contents($course->id, [
1547
            ['name' => 'modname', 'value' => 'page'],
1548
            ['name' => 'modid', 'value' => $pagecm->instance]
1549
        ]);
1550
 
1551
        // We need to execute the return values cleaning process to simulate the web service server.
1552
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1553
        $this->assertCount(1, $sections[0]['modules']);
1554
        $this->assertEquals('page', $sections[0]['modules'][0]['modname']);
1555
        $this->assertEquals($pagecm->downloadcontent, $sections[0]['modules'][0]['downloadcontent']);
1556
        $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $sections[0]['modules'][0]['downloadcontent']);
1557
    }
1558
 
1559
    /**
1560
     * Test get course contents completion manual
1561
     */
11 efrain 1562
    public function test_get_course_contents_completion_manual(): void {
1 efrain 1563
        global $CFG;
1564
        $this->resetAfterTest(true);
1565
 
1566
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) =
1567
            $this->prepare_get_course_contents_test();
1568
        availability_completion\condition::wipe_static_cache();
1569
 
1570
        // Test activity not completed yet.
1571
        $result = core_course_external::get_course_contents($course->id, array(
1572
            array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1573
        // We need to execute the return values cleaning process to simulate the web service server.
1574
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1575
 
1576
        $completiondata = $result[0]['modules'][0]["completiondata"];
1577
        $this->assertCount(1, $result[0]['modules']);
1578
        $this->assertEquals("forum", $result[0]['modules'][0]["modname"]);
1579
        $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1580
        $this->assertEquals(0, $completiondata['state']);
1581
        $this->assertEquals(0, $completiondata['timecompleted']);
1582
        $this->assertEmpty($completiondata['overrideby']);
1583
        $this->assertFalse($completiondata['valueused']);
1584
        $this->assertTrue($completiondata['hascompletion']);
1585
        $this->assertFalse($completiondata['isautomatic']);
1586
        $this->assertFalse($completiondata['istrackeduser']);
1587
        $this->assertTrue($completiondata['uservisible']);
1588
        $this->assertFalse($completiondata['isoverallcomplete']);
1589
 
1590
        // Set activity completed.
1591
        core_completion_external::update_activity_completion_status_manually($forumcm->id, true);
1592
 
1593
        $result = core_course_external::get_course_contents($course->id, array(
1594
            array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1595
        // We need to execute the return values cleaning process to simulate the web service server.
1596
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1597
 
1598
        $this->assertEquals(COMPLETION_COMPLETE, $result[0]['modules'][0]["completiondata"]['state']);
1599
        $this->assertTrue($result[0]['modules'][0]["completiondata"]['isoverallcomplete']);
1600
        $this->assertNotEmpty($result[0]['modules'][0]["completiondata"]['timecompleted']);
1601
        $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']);
1602
 
1603
        // Test activity with completion value that is used in an availability condition.
1604
        $result = core_course_external::get_course_contents($course->id, array(
1605
                array("name" => "modname", "value" => "label"), array("name" => "modid", "value" => $labelcm->instance)));
1606
        // We need to execute the return values cleaning process to simulate the web service server.
1607
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1608
 
1609
        $completiondata = $result[0]['modules'][0]["completiondata"];
1610
        $this->assertCount(1, $result[0]['modules']);
1611
        $this->assertEquals("label", $result[0]['modules'][0]["modname"]);
1612
        $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1613
        $this->assertEquals(0, $completiondata['state']);
1614
        $this->assertEquals(0, $completiondata['timecompleted']);
1615
        $this->assertEmpty($completiondata['overrideby']);
1616
        $this->assertTrue($completiondata['valueused']);
1617
        $this->assertTrue($completiondata['hascompletion']);
1618
        $this->assertFalse($completiondata['isautomatic']);
1619
        $this->assertFalse($completiondata['istrackeduser']);
1620
        $this->assertTrue($completiondata['uservisible']);
1621
        $this->assertFalse($completiondata['isoverallcomplete']);
1622
 
1623
        // Disable completion.
1624
        $CFG->enablecompletion = 0;
1625
        $result = core_course_external::get_course_contents($course->id, array(
1626
            array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1627
        // We need to execute the return values cleaning process to simulate the web service server.
1628
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1629
 
1630
        $this->assertArrayNotHasKey('completiondata', $result[0]['modules'][0]);
1631
    }
1632
 
1633
    /**
1634
     * Test get course contents completion auto
1635
     */
11 efrain 1636
    public function test_get_course_contents_completion_auto(): void {
1 efrain 1637
        global $CFG;
1638
        $this->resetAfterTest(true);
1639
 
1640
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) =
1641
            $this->prepare_get_course_contents_test();
1642
        availability_completion\condition::wipe_static_cache();
1643
 
1644
        // Test activity not completed yet.
1645
        $result = core_course_external::get_course_contents($course->id, [
1646
            [
1647
                "name" => "modname",
1648
                "value" => "forum"
1649
            ],
1650
            [
1651
                "name" => "modid",
1652
                "value" => $forumcompleteautocm->instance
1653
            ]
1654
        ]);
1655
        // We need to execute the return values cleaning process to simulate the web service server.
1656
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1657
 
1658
        $forummod = $result[0]['modules'][0];
1659
        $completiondata = $forummod["completiondata"];
1660
        $this->assertCount(1, $result[0]['modules']);
1661
        $this->assertEquals("forum", $forummod["modname"]);
1662
        $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $forummod["completion"]);
1663
        $this->assertEquals(0, $completiondata['state']);
1664
        $this->assertEquals(0, $completiondata['timecompleted']);
1665
        $this->assertEmpty($completiondata['overrideby']);
1666
        $this->assertFalse($completiondata['valueused']);
1667
        $this->assertTrue($completiondata['hascompletion']);
1668
        $this->assertTrue($completiondata['isautomatic']);
1669
        $this->assertFalse($completiondata['istrackeduser']);
1670
        $this->assertTrue($completiondata['uservisible']);
1671
        $this->assertCount(1, $completiondata['details']);
1672
        $this->assertFalse($completiondata['isoverallcomplete']);
1673
    }
1674
 
1675
    /**
1676
     * Test mimetype is returned for resources with showtype set.
1677
     */
11 efrain 1678
    public function test_get_course_contents_including_mimetype(): void {
1 efrain 1679
        $this->resetAfterTest(true);
1680
 
1681
        $this->setAdminUser();
1682
        $course = self::getDataGenerator()->create_course();
1683
 
1684
        $record = new stdClass();
1685
        $record->course = $course->id;
1686
        $record->showtype = 1;
1687
        $resource = self::getDataGenerator()->create_module('resource', $record);
1688
 
1689
        $result = core_course_external::get_course_contents($course->id);
1690
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1691
        $this->assertCount(1, $result[0]['modules']);   // One module, first section.
1692
        $customdata = json_decode($result[0]['modules'][0]['customdata']);
1693
        $displayoptions = unserialize($customdata->displayoptions);
1694
        $this->assertEquals('text/plain', $displayoptions['filedetails']['mimetype']);
1695
    }
1696
 
1697
    /**
1698
     * Test contents info is returned.
1699
     */
11 efrain 1700
    public function test_get_course_contents_contentsinfo(): void {
1 efrain 1701
        global $USER;
1702
 
1703
        $this->resetAfterTest(true);
1704
        $this->setAdminUser();
1705
        $timenow = time();
1706
 
1707
        $course = self::getDataGenerator()->create_course();
1708
 
1709
        $record = new stdClass();
1710
        $record->course = $course->id;
1711
        // One resource with one file.
1712
        $resource1 = self::getDataGenerator()->create_module('resource', $record);
1713
 
1714
        // More type of files.
1715
        $record->files = file_get_unused_draft_itemid();
1716
        $usercontext = context_user::instance($USER->id);
1717
        $extensions = array('txt', 'png', 'pdf');
1718
        $fs = get_file_storage();
1719
        foreach ($extensions as $key => $extension) {
1720
            // Add actual file there.
1721
            $filerecord = array('component' => 'user', 'filearea' => 'draft',
1722
                    'contextid' => $usercontext->id, 'itemid' => $record->files,
1723
                    'filename' => 'resource' . $key . '.' . $extension, 'filepath' => '/');
1724
            $fs->create_file_from_string($filerecord, 'Test resource ' . $key . ' file');
1725
        }
1726
 
1727
        // Create file reference.
1728
        $repos = repository::get_instances(array('type' => 'user'));
1729
        $userrepository = reset($repos);
1730
 
1731
        // Create a user private file.
1732
        $userfilerecord = new stdClass;
1733
        $userfilerecord->contextid = $usercontext->id;
1734
        $userfilerecord->component = 'user';
1735
        $userfilerecord->filearea  = 'private';
1736
        $userfilerecord->itemid    = 0;
1737
        $userfilerecord->filepath  = '/';
1738
        $userfilerecord->filename  = 'userfile.txt';
1739
        $userfilerecord->source    = 'test';
1740
        $userfile = $fs->create_file_from_string($userfilerecord, 'User file content');
1741
        $userfileref = $fs->pack_reference($userfilerecord);
1742
 
1743
        // Clone latest "normal" file.
1744
        $filerefrecord = clone (object) $filerecord;
1745
        $filerefrecord->filename = 'testref.txt';
1746
        $fileref = $fs->create_file_from_reference($filerefrecord, $userrepository->id, $userfileref);
1747
        // Set main file pointing to the file reference.
1748
        file_set_sortorder($usercontext->id, 'user', 'draft', $record->files, $filerefrecord->filepath,
1749
            $filerefrecord->filename, 1);
1750
 
1751
        // Once the reference has been created, create the file resource.
1752
        $resource2 = self::getDataGenerator()->create_module('resource', $record);
1753
 
1754
        $result = core_course_external::get_course_contents($course->id);
1755
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1756
        $this->assertCount(2, $result[0]['modules']);
1757
        foreach ($result[0]['modules'] as $module) {
1758
            if ($module['instance'] == $resource1->id) {
1759
                $this->assertEquals(1, $module['contentsinfo']['filescount']);
1760
                $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
1761
                $this->assertEquals($module['contents'][0]['filesize'], $module['contentsinfo']['filessize']);
1762
                $this->assertEquals(array('text/plain'), $module['contentsinfo']['mimetypes']);
1763
            } else {
1764
                $this->assertEquals(count($extensions) + 1, $module['contentsinfo']['filescount']);
1765
                $filessize = $module['contents'][0]['filesize'] + $module['contents'][1]['filesize'] +
1766
                    $module['contents'][2]['filesize'] + $module['contents'][3]['filesize'];
1767
                $this->assertEquals($filessize, $module['contentsinfo']['filessize']);
1768
                $this->assertEquals('user', $module['contentsinfo']['repositorytype']);
1769
                $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
1770
                $this->assertEquals(array('text/plain', 'image/png', 'application/pdf'), $module['contentsinfo']['mimetypes']);
1771
            }
1772
        }
1773
    }
1774
 
1775
    /**
1776
     * Test get_course_contents when hidden sections are displayed.
1777
     */
11 efrain 1778
    public function test_get_course_contents_hiddensections(): void {
1 efrain 1779
        global $DB;
1780
        $this->resetAfterTest(true);
1781
 
1782
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1783
        // Force returning hidden sections.
1784
        $course->hiddensections = 0;
1785
        update_course($course);
1786
 
1787
        $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
1788
        $user = self::getDataGenerator()->create_user();
1789
        self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
1790
        $this->setUser($user);
1791
 
1792
        $sections = core_course_external::get_course_contents($course->id, array());
1793
        // We need to execute the return values cleaning process to simulate the web service server.
1794
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1795
 
1796
        $this->assertCount(5, $sections); // All the sections, including the "not visible" one.
1797
        $this->assertCount(6, $sections[0]['modules']);
1798
        $this->assertCount(1, $sections[1]['modules']);
1799
        $this->assertCount(1, $sections[2]['modules']);
1800
        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1801
        $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
1802
 
1803
        $this->assertNotEmpty($sections[3]['availabilityinfo']);
1804
        $this->assertEquals(1, $sections[1]['section']);
1805
        $this->assertEquals(2, $sections[2]['section']);
1806
        $this->assertEquals(3, $sections[3]['section']);
1807
        // The module with the availability restriction met is returning contents.
1808
        $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
1809
        // The module with the availability restriction not met is not returning contents.
1810
        $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
1811
 
1812
        // Now include flag for returning stealth information (fake section).
1813
        $sections = core_course_external::get_course_contents($course->id,
1814
            array(array("name" => "includestealthmodules", "value" => 1)));
1815
        // We need to execute the return values cleaning process to simulate the web service server.
1816
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1817
 
1818
        $this->assertCount(6, $sections); // Include fake section with stealth activities.
1819
        $this->assertCount(6, $sections[0]['modules']);
1820
        $this->assertCount(1, $sections[1]['modules']);
1821
        $this->assertCount(1, $sections[2]['modules']);
1822
        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1823
        $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
1824
        $this->assertCount(1, $sections[5]['modules']); // One stealth module.
1825
        $this->assertEquals(-1, $sections[5]['id']);
1826
    }
1827
 
1828
    /**
1829
     * Test get course contents dates.
1830
     */
11 efrain 1831
    public function test_get_course_contents_dates(): void {
1 efrain 1832
        $this->resetAfterTest(true);
1833
 
1834
        $this->setAdminUser();
1835
        set_config('enablecourserelativedates', 1);
1836
 
1837
        // Course with just main section.
1838
        $timenow = time();
1839
        $course = self::getDataGenerator()->create_course(
1840
            ['numsections' => 0, 'relativedatesmode' => true, 'startdate' => $timenow - DAYSECS]);
1841
 
1842
        $teacher = self::getDataGenerator()->create_user();
1843
        self::getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
1844
 
1845
        $this->setUser($teacher);
1846
 
1847
        // Create resource (empty dates).
1848
        $resource = self::getDataGenerator()->create_module('resource', ['course' => $course->id]);
1849
        // Create activities with dates.
1850
        $resource = self::getDataGenerator()->create_module('forum', ['course' => $course->id, 'duedate' => $timenow]);
1851
        $resource = self::getDataGenerator()->create_module('choice',
1852
            ['course' => $course->id, 'timeopen' => $timenow, 'timeclose' => $timenow + DAYSECS]);
1853
        $resource = self::getDataGenerator()->create_module('assign',
1854
            ['course' => $course->id, 'allowsubmissionsfromdate' => $timenow]);
1855
 
1856
        $result = core_course_external::get_course_contents($course->id);
1857
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1858
 
1859
        foreach ($result[0]['modules'] as $module) {
1860
            if ($module['modname'] == 'resource') {
1861
                $this->assertEmpty($module['dates']);
1862
            } else if ($module['modname'] == 'forum') {
1863
                $this->assertCount(1, $module['dates']);
1864
                $this->assertEquals('duedate', $module['dates'][0]['dataid']);
1865
                $this->assertEquals($timenow, $module['dates'][0]['timestamp']);
1866
            } else if ($module['modname'] == 'choice') {
1867
                $this->assertCount(2, $module['dates']);
1868
                $this->assertEquals('timeopen', $module['dates'][0]['dataid']);
1869
                $this->assertEquals($timenow, $module['dates'][0]['timestamp']);
1870
                $this->assertEquals('timeclose', $module['dates'][1]['dataid']);
1871
                $this->assertEquals($timenow + DAYSECS, $module['dates'][1]['timestamp']);
1872
            } else if ($module['modname'] == 'assign') {
1873
                $this->assertCount(1, $module['dates']);
1874
                $this->assertEquals('allowsubmissionsfromdate', $module['dates'][0]['dataid']);
1875
                $this->assertEquals($timenow, $module['dates'][0]['timestamp']);
1876
                $this->assertEquals($course->startdate, $module['dates'][0]['relativeto']);
1877
            }
1878
        }
1879
    }
1880
 
1881
    /**
1882
     * Test get_course_contents for courses with invalid course format.
1883
     */
11 efrain 1884
    public function test_get_course_contents_invalid_format(): void {
1 efrain 1885
        global $DB;
1886
        $this->resetAfterTest();
1887
 
1888
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1889
 
1890
        $DB->set_field('course', 'format', 'fakeformat', ['id' => $course->id]);
1891
 
1892
        // WS should falback to default course format (topics) and avoid exceptions (but debugging will happen).
1893
        $result = core_course_external::get_course_contents($course->id);
1894
        $this->assertDebuggingCalled();
1895
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1896
    }
1897
 
1898
    /**
1441 ariadna 1899
     * Test get_course_contents for courses with sub-sections.
1900
     *
1901
     * @covers ::get_course_contents
1902
     */
1903
    public function test_get_course_contents_subsections(): void {
1904
        global $DB, $PAGE;
1905
        $this->resetAfterTest();
1906
 
1907
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1908
 
1909
        // Add subsection.
1910
        $modsubsection = $this->getDataGenerator()->create_module('subsection', ['course' => $course->id, 'section' => 2]);
1911
 
1912
        // This is needed until MDL-76728 is resolved.
1913
        $PAGE->set_url('/course/view.php', ['id' => $course->id]);
1914
 
1915
        $result = core_course_external::get_course_contents($course->id);
1916
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1917
 
1918
        $this->assertCount(5, $result); // We have 4 original sections plus the one created by mod_subsection.
1919
 
1920
        foreach ($result as $section) {
1921
 
1922
            if ($section['section'] == 5) { // This is the new section created by modsubsection.
1923
                $this->assertEquals('mod_subsection', $section['component']);
1924
                $this->assertEquals($modsubsection->id, $section['itemid']);
1925
            } else {
1926
                $this->assertEmpty($section['component']);
1927
                $this->assertEmpty($section['itemid']);
1928
            }
1929
 
1930
            if ($section['section'] == 2) { // This is the section where mod_subsection is.
1931
                foreach ($section['modules'] as $module) {
1932
                    if ($module['modname'] == 'subsection') {
1933
                        $this->assertNotEmpty($module['customdata']);
1934
                        $customdata = json_decode($module['customdata']);
1935
                        $lastsection = end($result);
1936
                        // Customdata contains the section id of the section created by the module.
1937
                        $this->assertEquals($lastsection['id'], $customdata->sectionid);
1938
                    }
1939
                }
1940
            }
1941
        }
1942
    }
1943
 
1944
    /**
1 efrain 1945
     * Test duplicate_course
1946
     */
11 efrain 1947
    public function test_duplicate_course(): void {
1 efrain 1948
        $this->resetAfterTest(true);
1949
 
1950
        // Create one course with three modules.
1951
        $course  = self::getDataGenerator()->create_course();
1952
        $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
1953
        $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1954
        $forumcontext = context_module::instance($forum->cmid);
1955
        $data = $this->getDataGenerator()->create_module('data', array('assessed'=>1, 'scale'=>100, 'course'=>$course->id));
1956
        $datacontext = context_module::instance($data->cmid);
1957
        $datacm = get_coursemodule_from_instance('page', $data->id);
1958
        $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
1959
        $pagecontext = context_module::instance($page->cmid);
1960
        $pagecm = get_coursemodule_from_instance('page', $page->id);
1961
 
1962
        // Set the required capabilities by the external function.
1963
        $coursecontext = context_course::instance($course->id);
1964
        $categorycontext = context_coursecat::instance($course->category);
1965
        $roleid = $this->assignUserCapability('moodle/course:create', $categorycontext->id);
1966
        $this->assignUserCapability('moodle/course:view', $categorycontext->id, $roleid);
1967
        $this->assignUserCapability('moodle/restore:restorecourse', $categorycontext->id, $roleid);
1968
        $this->assignUserCapability('moodle/backup:backupcourse', $coursecontext->id, $roleid);
1969
        $this->assignUserCapability('moodle/backup:configure', $coursecontext->id, $roleid);
1970
        // Optional capabilities to copy user data.
1971
        $this->assignUserCapability('moodle/backup:userinfo', $coursecontext->id, $roleid);
1972
        $this->assignUserCapability('moodle/restore:userinfo', $categorycontext->id, $roleid);
1973
 
1974
        $newcourse['fullname'] = 'Course duplicate';
1975
        $newcourse['shortname'] = 'courseduplicate';
1976
        $newcourse['categoryid'] = $course->category;
1977
        $newcourse['visible'] = true;
1978
        $newcourse['options'][] = array('name' => 'users', 'value' => true);
1979
 
1980
        $duplicate = core_course_external::duplicate_course($course->id, $newcourse['fullname'],
1981
                $newcourse['shortname'], $newcourse['categoryid'], $newcourse['visible'], $newcourse['options']);
1982
 
1983
        // We need to execute the return values cleaning process to simulate the web service server.
1984
        $duplicate = external_api::clean_returnvalue(core_course_external::duplicate_course_returns(), $duplicate);
1985
 
1986
        // Check that the course has been duplicated.
1987
        $this->assertEquals($newcourse['shortname'], $duplicate['shortname']);
1988
    }
1989
 
1990
    /**
1991
     * Test update_courses
1992
     */
11 efrain 1993
    public function test_update_courses(): void {
1 efrain 1994
        global $DB, $CFG, $USER, $COURSE;
1995
 
1996
        // Get current $COURSE to be able to restore it later (defaults to $SITE). We need this
1997
        // trick because we are both updating and getting (for testing) course information
1998
        // in the same request and core_course_external::update_courses()
1999
        // is overwriting $COURSE all over the time with OLD values, so later
2000
        // use of get_course() fetches those OLD values instead of the updated ones.
2001
        // See MDL-39723 for more info.
2002
        $origcourse = clone($COURSE);
2003
 
2004
        $this->resetAfterTest(true);
2005
 
2006
        // Set the required capabilities by the external function.
2007
        $contextid = context_system::instance()->id;
2008
        $roleid = $this->assignUserCapability('moodle/course:update', $contextid);
2009
        $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
2010
        $this->assignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
2011
        $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
2012
        $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
2013
        $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
2014
        $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
2015
        $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
2016
        $this->assignUserCapability('moodle/course:viewhiddencourses', $contextid, $roleid);
2017
        $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
2018
 
2019
        // Create category and courses.
2020
        $category1  = self::getDataGenerator()->create_category();
2021
        $category2  = self::getDataGenerator()->create_category();
2022
 
2023
        $originalcourse1 = self::getDataGenerator()->create_course();
2024
        self::getDataGenerator()->enrol_user($USER->id, $originalcourse1->id, $roleid);
2025
 
2026
        $originalcourse2 = self::getDataGenerator()->create_course();
2027
        self::getDataGenerator()->enrol_user($USER->id, $originalcourse2->id, $roleid);
2028
 
2029
        // Course with custom fields.
2030
        $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
2031
 
2032
        $fieldtext = self::getDataGenerator()->create_custom_field([
2033
            'categoryid' => $fieldcategory->get('id'), 'name' => 'Text', 'shortname' => 'text', 'type' => 'text', 'configdata' => [
2034
                'locked' => 1,
2035
            ],
2036
        ]);
2037
        $fieldtextarea = self::getDataGenerator()->create_custom_field([
2038
            'categoryid' => $fieldcategory->get('id'), 'name' => 'Textarea', 'shortname' => 'textarea', 'type' => 'textarea',
2039
        ]);
2040
 
2041
        $originalcourse3 = self::getDataGenerator()->create_course();
2042
        self::getDataGenerator()->enrol_user($USER->id, $originalcourse3->id, $roleid);
2043
 
2044
        // Course values to be updated.
2045
        $course1['id'] = $originalcourse1->id;
2046
        $course1['fullname'] = 'Updated test course 1';
2047
        $course1['shortname'] = 'Udestedtestcourse1';
2048
        $course1['categoryid'] = $category1->id;
2049
 
2050
        $course2['id'] = $originalcourse2->id;
2051
        $course2['fullname'] = 'Updated test course 2';
2052
        $course2['shortname'] = 'Updestedtestcourse2';
2053
        $course2['categoryid'] = $category2->id;
2054
        $course2['idnumber'] = 'Updatedidnumber2';
2055
        $course2['summary'] = 'Updaated description for course 2';
2056
        $course2['summaryformat'] = FORMAT_HTML;
2057
        $course2['format'] = 'topics';
2058
        $course2['showgrades'] = 1;
2059
        $course2['newsitems'] = 3;
2060
        $course2['startdate'] = 1420092000; // 01/01/2015.
2061
        $course2['enddate'] = 1422669600; // 01/31/2015.
2062
        $course2['maxbytes'] = 100000;
2063
        $course2['showreports'] = 1;
2064
        $course2['visible'] = 0;
2065
        $course2['hiddensections'] = 0;
2066
        $course2['groupmode'] = 0;
2067
        $course2['groupmodeforce'] = 0;
2068
        $course2['defaultgroupingid'] = 0;
2069
        $course2['enablecompletion'] = 1;
2070
        $course2['lang'] = 'en';
2071
        $course2['forcetheme'] = 'classic';
2072
 
2073
        $course3['id'] = $originalcourse3->id;
2074
        $course3['customfields'] = [
2075
            ['shortname' => $fieldtext->get('shortname'), 'value' => 'I long to see the sunlight in your hair'],
2076
            ['shortname' => $fieldtextarea->get('shortname'), 'value' => 'And tell you time and time again'],
2077
         ];
2078
 
2079
        $courses = array($course1, $course2, $course3);
2080
 
2081
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2082
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2083
                $updatedcoursewarnings);
2084
        $COURSE = $origcourse; // Restore $COURSE. Instead of using the OLD one set by the previous line.
2085
 
2086
        // Check that right number of courses were created.
2087
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2088
 
2089
        // Check that the courses were correctly created.
2090
        foreach ($courses as $course) {
2091
            $courseinfo = course_get_format($course['id'])->get_course();
2092
            $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course['id']);
2093
            if ($course['id'] == $course2['id']) {
2094
                $this->assertEquals($course2['fullname'], $courseinfo->fullname);
2095
                $this->assertEquals($course2['shortname'], $courseinfo->shortname);
2096
                $this->assertEquals($course2['categoryid'], $courseinfo->category);
2097
                $this->assertEquals($course2['idnumber'], $courseinfo->idnumber);
2098
                $this->assertEquals($course2['summary'], $courseinfo->summary);
2099
                $this->assertEquals($course2['summaryformat'], $courseinfo->summaryformat);
2100
                $this->assertEquals($course2['format'], $courseinfo->format);
2101
                $this->assertEquals($course2['showgrades'], $courseinfo->showgrades);
2102
                $this->assertEquals($course2['newsitems'], $courseinfo->newsitems);
2103
                $this->assertEquals($course2['startdate'], $courseinfo->startdate);
2104
                $this->assertEquals($course2['enddate'], $courseinfo->enddate);
2105
                $this->assertEquals($course2['maxbytes'], $courseinfo->maxbytes);
2106
                $this->assertEquals($course2['showreports'], $courseinfo->showreports);
2107
                $this->assertEquals($course2['visible'], $courseinfo->visible);
2108
                $this->assertEquals($course2['hiddensections'], $courseinfo->hiddensections);
2109
                $this->assertEquals($course2['groupmode'], $courseinfo->groupmode);
2110
                $this->assertEquals($course2['groupmodeforce'], $courseinfo->groupmodeforce);
2111
                $this->assertEquals($course2['defaultgroupingid'], $courseinfo->defaultgroupingid);
2112
                $this->assertEquals($course2['lang'], $courseinfo->lang);
2113
 
2114
                if (!empty($CFG->allowcoursethemes)) {
2115
                    $this->assertEquals($course2['forcetheme'], $courseinfo->theme);
2116
                }
2117
 
2118
                $this->assertEquals($course2['enablecompletion'], $courseinfo->enablecompletion);
2119
                $this->assertEquals((object) [
2120
                    'text' => null,
2121
                    'textarea' => null,
2122
                ], $customfields);
2123
            } else if ($course['id'] == $course1['id']) {
2124
                $this->assertEquals($course1['fullname'], $courseinfo->fullname);
2125
                $this->assertEquals($course1['shortname'], $courseinfo->shortname);
2126
                $this->assertEquals($course1['categoryid'], $courseinfo->category);
2127
                $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
2128
                $this->assertEquals('topics', $courseinfo->format);
2129
                $this->assertEquals(5, course_get_format($course['id'])->get_last_section_number());
2130
                $this->assertEquals(0, $courseinfo->newsitems);
2131
                $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
2132
                $this->assertEquals((object) [
2133
                    'text' => null,
2134
                    'textarea' => null,
2135
                ], $customfields);
2136
            } else if ($course['id'] == $course3['id']) {
2137
                $this->assertEquals((object) [
2138
                    'text' => 'I long to see the sunlight in your hair',
2139
                    'textarea' => '<div class="text_to_html">And tell you time and time again</div>',
2140
                ], $customfields);
2141
            } else {
2142
                throw new moodle_exception('Unexpected shortname');
2143
            }
2144
        }
2145
 
2146
        $courses = array($course1);
2147
        // Try update course without update capability.
2148
        $user = self::getDataGenerator()->create_user();
2149
        $this->setUser($user);
2150
        $this->unassignUserCapability('moodle/course:update', $contextid, $roleid);
2151
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2152
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2153
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2154
                                                                    $updatedcoursewarnings);
2155
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2156
 
2157
        // Try update course category without capability.
2158
        $this->assignUserCapability('moodle/course:update', $contextid, $roleid);
2159
        $this->unassignUserCapability('moodle/course:changecategory', $contextid, $roleid);
2160
        $user = self::getDataGenerator()->create_user();
2161
        $this->setUser($user);
2162
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2163
        $course1['categoryid'] = $category2->id;
2164
        $courses = array($course1);
2165
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2166
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2167
                                                                    $updatedcoursewarnings);
2168
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2169
 
2170
        // Try update course fullname without capability.
2171
        $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
2172
        $this->unassignUserCapability('moodle/course:changefullname', $contextid, $roleid);
2173
        $user = self::getDataGenerator()->create_user();
2174
        $this->setUser($user);
2175
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2176
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2177
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2178
                                                                    $updatedcoursewarnings);
2179
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2180
        $course1['fullname'] = 'Testing fullname without permission';
2181
        $courses = array($course1);
2182
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2183
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2184
                                                                    $updatedcoursewarnings);
2185
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2186
 
2187
        // Try update course shortname without capability.
2188
        $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
2189
        $this->unassignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
2190
        $user = self::getDataGenerator()->create_user();
2191
        $this->setUser($user);
2192
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2193
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2194
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2195
                                                                    $updatedcoursewarnings);
2196
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2197
        $course1['shortname'] = 'Testing shortname without permission';
2198
        $courses = array($course1);
2199
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2200
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2201
                                                                    $updatedcoursewarnings);
2202
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2203
 
2204
        // Try update course idnumber without capability.
2205
        $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
2206
        $this->unassignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
2207
        $user = self::getDataGenerator()->create_user();
2208
        $this->setUser($user);
2209
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2210
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2211
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2212
                                                                    $updatedcoursewarnings);
2213
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2214
        $course1['idnumber'] = 'NEWIDNUMBER';
2215
        $courses = array($course1);
2216
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2217
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2218
                                                                    $updatedcoursewarnings);
2219
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2220
 
2221
        // Try update course summary without capability.
2222
        $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
2223
        $this->unassignUserCapability('moodle/course:changesummary', $contextid, $roleid);
2224
        $user = self::getDataGenerator()->create_user();
2225
        $this->setUser($user);
2226
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2227
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2228
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2229
                                                                    $updatedcoursewarnings);
2230
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2231
        $course1['summary'] = 'New summary';
2232
        $courses = array($course1);
2233
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2234
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2235
                                                                    $updatedcoursewarnings);
2236
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2237
 
2238
        // Try update course with invalid summary format.
2239
        $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
2240
        $user = self::getDataGenerator()->create_user();
2241
        $this->setUser($user);
2242
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2243
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2244
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2245
                                                                    $updatedcoursewarnings);
2246
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2247
        $course1['summaryformat'] = 10;
2248
        $courses = array($course1);
2249
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2250
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2251
                                                                    $updatedcoursewarnings);
2252
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2253
 
2254
        // Try update course visibility without capability.
2255
        $this->unassignUserCapability('moodle/course:visibility', $contextid, $roleid);
2256
        $user = self::getDataGenerator()->create_user();
2257
        $this->setUser($user);
2258
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2259
        $course1['summaryformat'] = FORMAT_MOODLE;
2260
        $courses = array($course1);
2261
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2262
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2263
                                                                    $updatedcoursewarnings);
2264
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2265
        $course1['visible'] = 0;
2266
        $courses = array($course1);
2267
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2268
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2269
                                                                    $updatedcoursewarnings);
2270
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2271
 
2272
        // Try update course custom fields without capability.
2273
        $this->unassignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
2274
        $user = self::getDataGenerator()->create_user();
2275
        $this->setUser($user);
2276
        self::getDataGenerator()->enrol_user($user->id, $course3['id'], $roleid);
2277
 
2278
        $course3['customfields'] = [
2279
            ['shortname' => 'text', 'value' => 'New updated value'],
2280
        ];
2281
 
2282
        core_course_external::update_courses([$course3]);
2283
 
2284
        // Custom field was not updated.
2285
        $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course3['id']);
2286
        $this->assertEquals((object) [
2287
            'text' => 'I long to see the sunlight in your hair',
2288
            'textarea' => '<div class="text_to_html">And tell you time and time again</div>',
2289
        ], $customfields);
2290
    }
2291
 
2292
    /**
2293
     * Test delete course_module.
2294
     */
11 efrain 2295
    public function test_delete_modules(): void {
1 efrain 2296
        global $DB;
2297
 
2298
        // Ensure we reset the data after this test.
2299
        $this->resetAfterTest(true);
2300
 
2301
        // Create a user.
2302
        $user = self::getDataGenerator()->create_user();
2303
 
2304
        // Set the tests to run as the user.
2305
        self::setUser($user);
2306
 
2307
        // Create a course to add the modules.
2308
        $course = self::getDataGenerator()->create_course();
2309
 
2310
        // Create two test modules.
2311
        $record = new stdClass();
2312
        $record->course = $course->id;
2313
        $module1 = self::getDataGenerator()->create_module('forum', $record);
2314
        $module2 = self::getDataGenerator()->create_module('assign', $record);
2315
 
2316
        // Check the forum was correctly created.
2317
        $this->assertEquals(1, $DB->count_records('forum', array('id' => $module1->id)));
2318
 
2319
        // Check the assignment was correctly created.
2320
        $this->assertEquals(1, $DB->count_records('assign', array('id' => $module2->id)));
2321
 
2322
        // Check data exists in the course modules table.
2323
        $this->assertEquals(2, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
2324
                array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
2325
 
2326
        // Enrol the user in the course.
2327
        $enrol = enrol_get_plugin('manual');
2328
        $enrolinstances = enrol_get_instances($course->id, true);
2329
        foreach ($enrolinstances as $courseenrolinstance) {
2330
            if ($courseenrolinstance->enrol == "manual") {
2331
                $instance = $courseenrolinstance;
2332
                break;
2333
            }
2334
        }
2335
        $enrol->enrol_user($instance, $user->id);
2336
 
2337
        // Assign capabilities to delete module 1.
2338
        $modcontext = context_module::instance($module1->cmid);
2339
        $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id);
2340
 
2341
        // Assign capabilities to delete module 2.
2342
        $modcontext = context_module::instance($module2->cmid);
2343
        $newrole = create_role('Role 2', 'role2', 'Role 2 description');
2344
        $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id, $newrole);
2345
 
2346
        // Deleting these module instances.
2347
        core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2348
 
2349
        // Check the forum was deleted.
2350
        $this->assertEquals(0, $DB->count_records('forum', array('id' => $module1->id)));
2351
 
2352
        // Check the assignment was deleted.
2353
        $this->assertEquals(0, $DB->count_records('assign', array('id' => $module2->id)));
2354
 
2355
        // Check we retrieve no data in the course modules table.
2356
        $this->assertEquals(0, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
2357
                array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
2358
 
2359
        // Call with non-existent course module id and ensure exception thrown.
2360
        try {
2361
            core_course_external::delete_modules(array('1337'));
2362
            $this->fail('Exception expected due to missing course module.');
2363
        } catch (dml_missing_record_exception $e) {
2364
            $this->assertEquals('invalidcoursemodule', $e->errorcode);
2365
        }
2366
 
2367
        // Create two modules.
2368
        $module1 = self::getDataGenerator()->create_module('forum', $record);
2369
        $module2 = self::getDataGenerator()->create_module('assign', $record);
2370
 
2371
        // Since these modules were recreated the user will not have capabilities
2372
        // to delete them, ensure exception is thrown if they try.
2373
        try {
2374
            core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2375
            $this->fail('Exception expected due to missing capability.');
2376
        } catch (moodle_exception $e) {
2377
            $this->assertEquals('nopermissions', $e->errorcode);
2378
        }
2379
 
2380
        // Unenrol user from the course.
2381
        $enrol->unenrol_user($instance, $user->id);
2382
 
2383
        // Try and delete modules from the course the user was unenrolled in, make sure exception thrown.
2384
        try {
2385
            core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2386
            $this->fail('Exception expected due to being unenrolled from the course.');
2387
        } catch (moodle_exception $e) {
2388
            $this->assertEquals('requireloginerror', $e->errorcode);
2389
        }
2390
    }
2391
 
2392
    /**
2393
     * Test import_course into an empty course
2394
     */
11 efrain 2395
    public function test_import_course_empty(): void {
1 efrain 2396
        global $USER;
2397
 
2398
        $this->resetAfterTest(true);
2399
 
2400
        $course1  = self::getDataGenerator()->create_course();
2401
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id, 'name' => 'Forum test'));
2402
        $page = $this->getDataGenerator()->create_module('page', array('course' => $course1->id, 'name' => 'Page test'));
2403
 
2404
        $course2  = self::getDataGenerator()->create_course();
2405
 
2406
        $course1cms = get_fast_modinfo($course1->id)->get_cms();
2407
        $course2cms = get_fast_modinfo($course2->id)->get_cms();
2408
 
2409
        // Verify the state of the courses before we do the import.
2410
        $this->assertCount(2, $course1cms);
2411
        $this->assertEmpty($course2cms);
2412
 
2413
        // Setup the user to run the operation (ugly hack because validate_context() will
2414
        // fail as the email is not set by $this->setAdminUser()).
2415
        $this->setAdminUser();
2416
        $USER->email = 'emailtopass@example.com';
2417
 
2418
        // Import from course1 to course2.
2419
        core_course_external::import_course($course1->id, $course2->id, 0);
2420
 
2421
        // Verify that now we have two modules in both courses.
2422
        $course1cms = get_fast_modinfo($course1->id)->get_cms();
2423
        $course2cms = get_fast_modinfo($course2->id)->get_cms();
2424
        $this->assertCount(2, $course1cms);
2425
        $this->assertCount(2, $course2cms);
2426
 
2427
        // Verify that the names transfered across correctly.
2428
        foreach ($course2cms as $cm) {
2429
            if ($cm->modname === 'page') {
2430
                $this->assertEquals($cm->name, $page->name);
2431
            } else if ($cm->modname === 'forum') {
2432
                $this->assertEquals($cm->name, $forum->name);
2433
            } else {
2434
                $this->fail('Unknown CM found.');
2435
            }
2436
        }
2437
    }
2438
 
2439
    /**
2440
     * Test import_course into an filled course
2441
     */
11 efrain 2442
    public function test_import_course_filled(): void {
1 efrain 2443
        global $USER;
2444
 
2445
        $this->resetAfterTest(true);
2446
 
2447
        // Add forum and page to course1.
2448
        $course1  = self::getDataGenerator()->create_course();
2449
        $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2450
        $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
2451
 
2452
        // Add quiz to course 2.
2453
        $course2  = self::getDataGenerator()->create_course();
2454
        $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
2455
 
2456
        $course1cms = get_fast_modinfo($course1->id)->get_cms();
2457
        $course2cms = get_fast_modinfo($course2->id)->get_cms();
2458
 
2459
        // Verify the state of the courses before we do the import.
2460
        $this->assertCount(2, $course1cms);
2461
        $this->assertCount(1, $course2cms);
2462
 
2463
        // Setup the user to run the operation (ugly hack because validate_context() will
2464
        // fail as the email is not set by $this->setAdminUser()).
2465
        $this->setAdminUser();
2466
        $USER->email = 'emailtopass@example.com';
2467
 
2468
        // Import from course1 to course2 without deleting content.
2469
        core_course_external::import_course($course1->id, $course2->id, 0);
2470
 
2471
        $course2cms = get_fast_modinfo($course2->id)->get_cms();
2472
 
2473
        // Verify that now we have three modules in course2.
2474
        $this->assertCount(3, $course2cms);
2475
 
2476
        // Verify that the names transfered across correctly.
2477
        foreach ($course2cms as $cm) {
2478
            if ($cm->modname === 'page') {
2479
                $this->assertEquals($cm->name, $page->name);
2480
            } else if ($cm->modname === 'forum') {
2481
                $this->assertEquals($cm->name, $forum->name);
2482
            } else if ($cm->modname === 'quiz') {
2483
                $this->assertEquals($cm->name, $quiz->name);
2484
            } else {
2485
                $this->fail('Unknown CM found.');
2486
            }
2487
        }
2488
    }
2489
 
2490
    /**
2491
     * Test import_course with only blocks set to backup
2492
     */
11 efrain 2493
    public function test_import_course_blocksonly(): void {
1 efrain 2494
        global $USER, $DB;
2495
 
2496
        $this->resetAfterTest(true);
2497
 
2498
        // Add forum and page to course1.
2499
        $course1  = self::getDataGenerator()->create_course();
2500
        $course1ctx = context_course::instance($course1->id);
2501
        $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2502
        $block = $this->getDataGenerator()->create_block('online_users', array('parentcontextid' => $course1ctx->id));
2503
 
2504
        $course2  = self::getDataGenerator()->create_course();
2505
        $course2ctx = context_course::instance($course2->id);
2506
        $initialblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
2507
        $initialcmcount = count(get_fast_modinfo($course2->id)->get_cms());
2508
 
2509
        // Setup the user to run the operation (ugly hack because validate_context() will
2510
        // fail as the email is not set by $this->setAdminUser()).
2511
        $this->setAdminUser();
2512
        $USER->email = 'emailtopass@example.com';
2513
 
2514
        // Import from course1 to course2 without deleting content, but excluding
2515
        // activities.
2516
        $options = array(
2517
            array('name' => 'activities', 'value' => 0),
2518
            array('name' => 'blocks', 'value' => 1),
2519
            array('name' => 'filters', 'value' => 0),
2520
        );
2521
 
2522
        core_course_external::import_course($course1->id, $course2->id, 0, $options);
2523
 
2524
        $newcmcount = count(get_fast_modinfo($course2->id)->get_cms());
2525
        $newblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
2526
        // Check that course modules haven't changed, but that blocks have.
2527
        $this->assertEquals($initialcmcount, $newcmcount);
2528
        $this->assertEquals(($initialblockcount + 1), $newblockcount);
2529
    }
2530
 
2531
    /**
2532
     * Test import_course into an filled course, deleting content.
2533
     */
11 efrain 2534
    public function test_import_course_deletecontent(): void {
1 efrain 2535
        global $USER;
2536
        $this->resetAfterTest(true);
2537
 
2538
        // Add forum and page to course1.
2539
        $course1  = self::getDataGenerator()->create_course();
2540
        $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2541
        $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
2542
 
2543
        // Add quiz to course 2.
2544
        $course2  = self::getDataGenerator()->create_course();
2545
        $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
2546
 
2547
        $course1cms = get_fast_modinfo($course1->id)->get_cms();
2548
        $course2cms = get_fast_modinfo($course2->id)->get_cms();
2549
 
2550
        // Verify the state of the courses before we do the import.
2551
        $this->assertCount(2, $course1cms);
2552
        $this->assertCount(1, $course2cms);
2553
 
2554
        // Setup the user to run the operation (ugly hack because validate_context() will
2555
        // fail as the email is not set by $this->setAdminUser()).
2556
        $this->setAdminUser();
2557
        $USER->email = 'emailtopass@example.com';
2558
 
2559
        // Import from course1 to course2,  deleting content.
2560
        core_course_external::import_course($course1->id, $course2->id, 1);
2561
 
2562
        $course2cms = get_fast_modinfo($course2->id)->get_cms();
2563
 
2564
        // Verify that now we have two modules in course2.
2565
        $this->assertCount(2, $course2cms);
2566
 
2567
        // Verify that the course only contains the imported modules.
2568
        foreach ($course2cms as $cm) {
2569
            if ($cm->modname === 'page') {
2570
                $this->assertEquals($cm->name, $page->name);
2571
            } else if ($cm->modname === 'forum') {
2572
                $this->assertEquals($cm->name, $forum->name);
2573
            } else {
2574
                $this->fail('Unknown CM found: '.$cm->name);
2575
            }
2576
        }
2577
    }
2578
 
2579
    /**
2580
     * Ensure import_course handles incorrect deletecontent option correctly.
2581
     */
11 efrain 2582
    public function test_import_course_invalid_deletecontent_option(): void {
1 efrain 2583
        $this->resetAfterTest(true);
2584
 
2585
        $course1  = self::getDataGenerator()->create_course();
2586
        $course2  = self::getDataGenerator()->create_course();
2587
 
2588
        $this->expectException('moodle_exception');
2589
        $this->expectExceptionMessage(get_string('invalidextparam', 'webservice', -1));
2590
        // Import from course1 to course2, with invalid option
2591
        core_course_external::import_course($course1->id, $course2->id, -1);;
2592
    }
2593
 
2594
    /**
2595
     * Test view_course function
2596
     */
11 efrain 2597
    public function test_view_course(): void {
1 efrain 2598
 
2599
        $this->resetAfterTest();
2600
 
2601
        // Course without sections.
2602
        $course = $this->getDataGenerator()->create_course(array('numsections' => 5), array('createsections' => true));
2603
        $this->setAdminUser();
2604
 
2605
        // Redirect events to the sink, so we can recover them later.
2606
        $sink = $this->redirectEvents();
2607
 
2608
        $result = core_course_external::view_course($course->id, 1);
2609
        $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
2610
        $events = $sink->get_events();
2611
        $event = reset($events);
2612
 
2613
        // Check the event details are correct.
2614
        $this->assertInstanceOf('\core\event\course_viewed', $event);
2615
        $this->assertEquals(context_course::instance($course->id), $event->get_context());
2616
        $this->assertEquals(1, $event->other['coursesectionnumber']);
2617
 
2618
        $result = core_course_external::view_course($course->id);
2619
        $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
2620
        $events = $sink->get_events();
2621
        $event = array_pop($events);
2622
        $sink->close();
2623
 
2624
        // Check the event details are correct.
2625
        $this->assertInstanceOf('\core\event\course_viewed', $event);
2626
        $this->assertEquals(context_course::instance($course->id), $event->get_context());
2627
        $this->assertEmpty($event->other);
2628
 
2629
    }
2630
 
2631
    /**
2632
     * Test get_course_module
2633
     */
11 efrain 2634
    public function test_get_course_module(): void {
1 efrain 2635
        global $DB;
2636
 
2637
        $this->resetAfterTest(true);
2638
 
2639
        $this->setAdminUser();
2640
        $course = self::getDataGenerator()->create_course(['enablecompletion' => 1]);
2641
        $record = array(
2642
            'course' => $course->id,
2643
            'name' => 'First Assignment'
2644
        );
2645
        $options = array(
2646
            'idnumber' => 'ABC',
2647
            'visible' => 0,
2648
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
2649
            'completiongradeitemnumber' => 0,
2650
            'completionpassgrade' => 1,
2651
        );
2652
        // Hidden activity.
2653
        $assign = self::getDataGenerator()->create_module('assign', $record, $options);
2654
 
2655
        $outcomescale = 'Distinction, Very Good, Good, Pass, Fail';
2656
 
2657
        // Insert a custom grade scale to be used by an outcome.
2658
        $gradescale = new grade_scale();
2659
        $gradescale->name        = 'gettcoursemodulescale';
2660
        $gradescale->courseid    = $course->id;
2661
        $gradescale->userid      = 0;
2662
        $gradescale->scale       = $outcomescale;
2663
        $gradescale->description = 'This scale is used to mark standard assignments.';
2664
        $gradescale->insert();
2665
 
2666
        // Insert an outcome.
2667
        $data = new stdClass();
2668
        $data->courseid = $course->id;
2669
        $data->fullname = 'Team work';
2670
        $data->shortname = 'Team work';
2671
        $data->scaleid = $gradescale->id;
2672
        $outcome = new grade_outcome($data, false);
2673
        $outcome->insert();
2674
 
2675
        $outcomegradeitem = new grade_item();
2676
        $outcomegradeitem->itemname = $outcome->shortname;
2677
        $outcomegradeitem->itemtype = 'mod';
2678
        $outcomegradeitem->itemmodule = 'assign';
2679
        $outcomegradeitem->iteminstance = $assign->id;
2680
        $outcomegradeitem->outcomeid = $outcome->id;
2681
        $outcomegradeitem->cmid = 0;
2682
        $outcomegradeitem->courseid = $course->id;
2683
        $outcomegradeitem->aggregationcoef = 0;
2684
        $outcomegradeitem->itemnumber = 1000; // Outcomes start at 1000.
2685
        $outcomegradeitem->gradetype = GRADE_TYPE_SCALE;
2686
        $outcomegradeitem->scaleid = $outcome->scaleid;
2687
        $outcomegradeitem->insert();
2688
 
2689
        $assignmentgradeitem = grade_item::fetch(
2690
            array(
2691
                'itemtype' => 'mod',
2692
                'itemmodule' => 'assign',
2693
                'iteminstance' => $assign->id,
2694
                'itemnumber' => 0,
2695
                'courseid' => $course->id
2696
            )
2697
        );
2698
        $outcomegradeitem->set_parent($assignmentgradeitem->categoryid);
2699
        $outcomegradeitem->move_after_sortorder($assignmentgradeitem->sortorder);
2700
 
2701
        // Test admin user can see the complete hidden activity.
2702
        $result = core_course_external::get_course_module($assign->cmid);
2703
        $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
2704
 
2705
        $this->assertCount(0, $result['warnings']);
2706
        // Test we retrieve all the fields.
2707
        $this->assertCount(30, $result['cm']);
2708
        $this->assertEquals($record['name'], $result['cm']['name']);
2709
        $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
2710
        $this->assertEquals(100, $result['cm']['grade']);
2711
        $this->assertEquals(0.0, $result['cm']['gradepass']);
2712
        $this->assertEquals('submissions', $result['cm']['advancedgrading'][0]['area']);
2713
        $this->assertEmpty($result['cm']['advancedgrading'][0]['method']);
2714
        $this->assertEquals($outcomescale, $result['cm']['outcomes'][0]['scale']);
2715
        $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $result['cm']['downloadcontent']);
2716
 
2717
        $student = $this->getDataGenerator()->create_user();
2718
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2719
 
2720
        self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
2721
        $this->setUser($student);
2722
 
2723
        // The user shouldn't be able to see the activity.
2724
        try {
2725
            core_course_external::get_course_module($assign->cmid);
2726
            $this->fail('Exception expected due to invalid permissions.');
2727
        } catch (moodle_exception $e) {
2728
            $this->assertEquals('requireloginerror', $e->errorcode);
2729
        }
2730
 
2731
        // Make module visible.
2732
        set_coursemodule_visible($assign->cmid, 1);
2733
 
2734
        // Test student user.
2735
        $result = core_course_external::get_course_module($assign->cmid);
2736
        $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
2737
 
2738
        $this->assertCount(0, $result['warnings']);
2739
        // Test we retrieve only the few files we can see.
2740
        $this->assertCount(12, $result['cm']);
2741
        $this->assertEquals($assign->cmid, $result['cm']['id']);
2742
        $this->assertEquals($course->id, $result['cm']['course']);
2743
        $this->assertEquals('assign', $result['cm']['modname']);
2744
        $this->assertEquals($assign->id, $result['cm']['instance']);
2745
 
2746
    }
2747
 
2748
    /**
2749
     * Test get_course_module_by_instance
2750
     */
11 efrain 2751
    public function test_get_course_module_by_instance(): void {
1 efrain 2752
        global $DB;
2753
 
2754
        $this->resetAfterTest(true);
2755
 
2756
        $this->setAdminUser();
2757
        $course = self::getDataGenerator()->create_course();
2758
        $record = array(
2759
            'course' => $course->id,
2760
            'name' => 'First quiz',
2761
            'grade' => 90.00
2762
        );
2763
        $options = array(
2764
            'idnumber' => 'ABC',
2765
            'visible' => 0
2766
        );
2767
        // Hidden activity.
2768
        $quiz = self::getDataGenerator()->create_module('quiz', $record, $options);
2769
 
2770
        // Test admin user can see the complete hidden activity.
2771
        $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2772
        $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
2773
 
2774
        $this->assertCount(0, $result['warnings']);
2775
        // Test we retrieve all the fields.
2776
        $this->assertCount(28, $result['cm']);
2777
        $this->assertEquals($record['name'], $result['cm']['name']);
2778
        $this->assertEquals($record['grade'], $result['cm']['grade']);
2779
        $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
2780
        $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $result['cm']['downloadcontent']);
2781
 
2782
        $student = $this->getDataGenerator()->create_user();
2783
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2784
 
2785
        self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
2786
        $this->setUser($student);
2787
 
2788
        // The user shouldn't be able to see the activity.
2789
        try {
2790
            core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2791
            $this->fail('Exception expected due to invalid permissions.');
2792
        } catch (moodle_exception $e) {
2793
            $this->assertEquals('requireloginerror', $e->errorcode);
2794
        }
2795
 
2796
        // Make module visible.
2797
        set_coursemodule_visible($quiz->cmid, 1);
2798
 
2799
        // Test student user.
2800
        $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2801
        $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
2802
 
2803
        $this->assertCount(0, $result['warnings']);
2804
        // Test we retrieve only the few files we can see.
2805
        $this->assertCount(12, $result['cm']);
2806
        $this->assertEquals($quiz->cmid, $result['cm']['id']);
2807
        $this->assertEquals($course->id, $result['cm']['course']);
2808
        $this->assertEquals('quiz', $result['cm']['modname']);
2809
        $this->assertEquals($quiz->id, $result['cm']['instance']);
2810
 
2811
        // Try with an invalid module name.
2812
        try {
2813
            core_course_external::get_course_module_by_instance('abc', $quiz->id);
2814
            $this->fail('Exception expected due to invalid module name.');
2815
        } catch (dml_read_exception $e) {
2816
            $this->assertEquals('dmlreadexception', $e->errorcode);
2817
        }
2818
 
2819
    }
2820
 
2821
    /**
2822
     * Test get_user_navigation_options
2823
     */
11 efrain 2824
    public function test_get_user_navigation_options(): void {
1 efrain 2825
        global $USER;
2826
 
2827
        $this->resetAfterTest();
2828
        $course1 = self::getDataGenerator()->create_course();
2829
        $course2 = self::getDataGenerator()->create_course();
2830
 
2831
        // Create a viewer user.
2832
        $viewer = self::getDataGenerator()->create_user();
2833
        $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2834
        $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2835
 
2836
        $this->setUser($viewer->id);
2837
        $courses = array($course1->id , $course2->id, SITEID);
2838
 
2839
        $result = core_course_external::get_user_navigation_options($courses);
2840
        $result = external_api::clean_returnvalue(core_course_external::get_user_navigation_options_returns(), $result);
2841
 
2842
        $this->assertCount(0, $result['warnings']);
2843
        $this->assertCount(3, $result['courses']);
2844
 
2845
        foreach ($result['courses'] as $course) {
2846
            $navoptions = new stdClass;
2847
            foreach ($course['options'] as $option) {
2848
                $navoptions->{$option['name']} = $option['available'];
2849
            }
1441 ariadna 2850
            $this->assertCount(10, $course['options']);
1 efrain 2851
            if ($course['id'] == SITEID) {
2852
                $this->assertTrue($navoptions->blogs);
2853
                $this->assertFalse($navoptions->notes);
2854
                $this->assertFalse($navoptions->participants);
2855
                $this->assertTrue($navoptions->badges);
2856
                $this->assertTrue($navoptions->tags);
2857
                $this->assertFalse($navoptions->grades);
2858
                $this->assertFalse($navoptions->search);
2859
                $this->assertTrue($navoptions->competencies);
2860
                $this->assertFalse($navoptions->communication);
1441 ariadna 2861
                $this->assertFalse($navoptions->overview);
1 efrain 2862
            } else {
2863
                $this->assertTrue($navoptions->blogs);
2864
                $this->assertFalse($navoptions->notes);
2865
                $this->assertTrue($navoptions->participants);
2866
                $this->assertFalse($navoptions->badges);
2867
                $this->assertFalse($navoptions->tags);
2868
                $this->assertTrue($navoptions->grades);
2869
                $this->assertFalse($navoptions->search);
2870
                $this->assertTrue($navoptions->competencies);
2871
                $this->assertFalse($navoptions->communication);
1441 ariadna 2872
                $this->assertTrue($navoptions->overview);
1 efrain 2873
            }
2874
        }
2875
    }
2876
 
2877
    /**
2878
     * Test get_user_administration_options
2879
     */
11 efrain 2880
    public function test_get_user_administration_options(): void {
1 efrain 2881
        global $USER;
2882
 
2883
        $this->resetAfterTest();
2884
        $course1 = self::getDataGenerator()->create_course();
2885
        $course2 = self::getDataGenerator()->create_course();
2886
 
2887
        // Create a viewer user.
2888
        $viewer = self::getDataGenerator()->create_user();
2889
        $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2890
        $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2891
 
2892
        $this->setUser($viewer->id);
2893
        $courses = array($course1->id , $course2->id, SITEID);
2894
 
2895
        $result = core_course_external::get_user_administration_options($courses);
2896
        $result = external_api::clean_returnvalue(core_course_external::get_user_administration_options_returns(), $result);
2897
 
2898
        $this->assertCount(0, $result['warnings']);
2899
        $this->assertCount(3, $result['courses']);
2900
 
2901
        foreach ($result['courses'] as $course) {
2902
            $adminoptions = new stdClass;
2903
            foreach ($course['options'] as $option) {
2904
                $adminoptions->{$option['name']} = $option['available'];
2905
            }
2906
            if ($course['id'] == SITEID) {
2907
                $this->assertCount(17, $course['options']);
2908
                $this->assertFalse($adminoptions->update);
2909
                $this->assertFalse($adminoptions->filters);
2910
                $this->assertFalse($adminoptions->reports);
2911
                $this->assertFalse($adminoptions->backup);
2912
                $this->assertFalse($adminoptions->restore);
2913
                $this->assertFalse($adminoptions->files);
2914
                $this->assertFalse(!isset($adminoptions->tags));
2915
                $this->assertFalse($adminoptions->gradebook);
2916
                $this->assertFalse($adminoptions->outcomes);
2917
                $this->assertFalse($adminoptions->badges);
2918
                $this->assertFalse($adminoptions->import);
2919
                $this->assertFalse($adminoptions->reset);
2920
                $this->assertFalse($adminoptions->roles);
2921
                $this->assertFalse($adminoptions->editcompletion);
2922
                $this->assertFalse($adminoptions->copy);
2923
            } else {
2924
                $this->assertCount(15, $course['options']);
2925
                $this->assertFalse($adminoptions->update);
2926
                $this->assertFalse($adminoptions->filters);
2927
                $this->assertFalse($adminoptions->reports);
2928
                $this->assertFalse($adminoptions->backup);
2929
                $this->assertFalse($adminoptions->restore);
2930
                $this->assertFalse($adminoptions->files);
2931
                $this->assertFalse($adminoptions->tags);
2932
                $this->assertFalse($adminoptions->gradebook);
2933
                $this->assertFalse($adminoptions->outcomes);
2934
                $this->assertTrue($adminoptions->badges);
2935
                $this->assertFalse($adminoptions->import);
2936
                $this->assertFalse($adminoptions->reset);
2937
                $this->assertFalse($adminoptions->roles);
2938
                $this->assertFalse($adminoptions->editcompletion);
2939
                $this->assertFalse($adminoptions->copy);
2940
            }
2941
        }
2942
    }
2943
 
2944
    /**
2945
     * Test get_courses_by_fields
2946
     */
11 efrain 2947
    public function test_get_courses_by_field(): void {
1 efrain 2948
        global $DB, $USER;
2949
        $this->resetAfterTest(true);
2950
 
2951
        $this->setAdminUser();
2952
 
2953
        $category1 = self::getDataGenerator()->create_category(array('name' => 'Cat 1'));
2954
        $category2 = self::getDataGenerator()->create_category(array('parent' => $category1->id));
1441 ariadna 2955
        $numsections = 4;
2956
        $course1 = self::getDataGenerator()->create_course([
2957
            'category' => $category1->id,
2958
            'shortname' => 'c1',
2959
            'format' => 'topics',
2960
            'numsections' => $numsections,
2961
        ]);
1 efrain 2962
 
2963
        $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
2964
        $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
2965
            'categoryid' => $fieldcategory->get('id')];
2966
        $field = self::getDataGenerator()->create_custom_field($customfield);
2967
        $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
2968
        // Create course image.
2969
        $draftid = file_get_unused_draft_itemid();
2970
        $filerecord = [
2971
            'component' => 'user',
2972
            'filearea' => 'draft',
2973
            'contextid' => context_user::instance($USER->id)->id,
2974
            'itemid' => $draftid,
2975
            'filename' => 'image.jpg',
2976
            'filepath' => '/',
2977
        ];
2978
        $fs = get_file_storage();
1441 ariadna 2979
        $fs->create_file_from_pathname($filerecord, self::get_fixture_path('core_course', 'image.jpg'));
1 efrain 2980
        $course2 = self::getDataGenerator()->create_course([
2981
            'visible' => 0,
2982
            'category' => $category2->id,
2983
            'idnumber' => 'i2',
2984
            'customfields' => [$customfieldvalue],
2985
            'overviewfiles_filemanager' => $draftid
2986
        ]);
2987
 
2988
        $student1 = self::getDataGenerator()->create_user();
2989
        $user1 = self::getDataGenerator()->create_user();
2990
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2991
        self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id);
2992
        self::getDataGenerator()->enrol_user($student1->id, $course2->id, $studentrole->id);
2993
 
2994
        self::setAdminUser();
2995
        // As admins, we should be able to retrieve everything.
2996
        $result = core_course_external::get_courses_by_field();
2997
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2998
        $this->assertCount(3, $result['courses']);
2999
        // Expect to receive all the fields.
3000
        $this->assertCount(41, $result['courses'][0]);
3001
        $this->assertCount(42, $result['courses'][1]);  // One more field because is not the site course.
3002
        $this->assertCount(42, $result['courses'][2]);  // One more field because is not the site course.
3003
 
3004
        $result = core_course_external::get_courses_by_field('id', $course1->id);
3005
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3006
        $this->assertCount(1, $result['courses']);
3007
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3008
        // Expect to receive all the fields.
3009
        $this->assertCount(42, $result['courses'][0]);
3010
        // Check default values for course format topics.
3011
        $this->assertCount(3, $result['courses'][0]['courseformatoptions']);
3012
        foreach ($result['courses'][0]['courseformatoptions'] as $option) {
3013
            switch ($option['name']) {
3014
                case 'hiddensections':
3015
                    $this->assertEquals(1, $option['value']);
3016
                    break;
3017
                case 'coursedisplay':
3018
                    $this->assertEquals(0, $option['value']);
3019
                    break;
3020
                case 'indentation':
3021
                    $this->assertEquals(1, $option['value']);
3022
                    break;
3023
                default:
3024
            }
3025
        }
3026
        $this->assertStringContainsString('/course/generated', $result['courses'][0]['courseimage']);
3027
 
3028
        $result = core_course_external::get_courses_by_field('id', $course2->id);
3029
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3030
        $this->assertCount(1, $result['courses']);
3031
        $this->assertEquals($course2->id, $result['courses'][0]['id']);
3032
        // Check custom fields properly returned.
3033
        $this->assertEquals([
3034
            'shortname' => $customfield['shortname'],
3035
            'name' => $customfield['name'],
3036
            'type' => $customfield['type'],
3037
            'value' => $customfieldvalue['value'],
3038
            'valueraw' => $customfieldvalue['value'],
3039
        ], $result['courses'][0]['customfields'][0]);
3040
        $this->assertStringContainsString('/course/overviewfiles', $result['courses'][0]['courseimage']);
3041
 
3042
        $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
3043
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3044
        $this->assertCount(2, $result['courses']);
3045
 
3046
        // Check default filters.
3047
        $this->assertCount(6, $result['courses'][0]['filters']);
3048
        $this->assertCount(6, $result['courses'][1]['filters']);
3049
 
3050
        $result = core_course_external::get_courses_by_field('category', $category1->id);
3051
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3052
        $this->assertCount(1, $result['courses']);
3053
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3054
        $this->assertEquals('Cat 1', $result['courses'][0]['categoryname']);
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(1, $result['courses']);
3064
        $this->assertEquals($course2->id, $result['courses'][0]['id']);
3065
 
3066
        $result = core_course_external::get_courses_by_field('idnumber', 'x');
3067
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3068
        $this->assertCount(0, $result['courses']);
3069
 
3070
        // Change filter value.
3071
        filter_set_local_state('mediaplugin', context_course::instance($course1->id)->id, TEXTFILTER_OFF);
3072
 
3073
        self::setUser($student1);
3074
        // All visible courses  (including front page) for normal student.
3075
        $result = core_course_external::get_courses_by_field();
3076
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3077
        $this->assertCount(2, $result['courses']);
3078
        $this->assertCount(34, $result['courses'][0]);
3079
        $this->assertCount(35, $result['courses'][1]);  // One field more (course format options), not present in site course.
3080
 
3081
        $result = core_course_external::get_courses_by_field('id', $course1->id);
3082
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3083
        $this->assertCount(1, $result['courses']);
3084
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3085
        // Expect to receive all the files that a student can see.
3086
        $this->assertCount(35, $result['courses'][0]);
3087
 
3088
        // Check default filters.
3089
        $filters = $result['courses'][0]['filters'];
3090
        $this->assertCount(6, $filters);
3091
        $found = false;
3092
        foreach ($filters as $filter) {
3093
            if ($filter['filter'] == 'mediaplugin' and $filter['localstate'] == TEXTFILTER_OFF) {
3094
                $found = true;
3095
            }
3096
        }
3097
        $this->assertTrue($found);
3098
 
3099
        // Course 2 is not visible.
3100
        $result = core_course_external::get_courses_by_field('id', $course2->id);
3101
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3102
        $this->assertCount(0, $result['courses']);
3103
 
3104
        $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
3105
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3106
        $this->assertCount(1, $result['courses']);
3107
 
3108
        $result = core_course_external::get_courses_by_field('category', $category1->id);
3109
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3110
        $this->assertCount(1, $result['courses']);
3111
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3112
 
3113
        $result = core_course_external::get_courses_by_field('shortname', 'c1');
3114
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3115
        $this->assertCount(1, $result['courses']);
3116
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3117
 
3118
        $result = core_course_external::get_courses_by_field('idnumber', 'i2');
3119
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3120
        $this->assertCount(0, $result['courses']);
3121
 
3122
        $result = core_course_external::get_courses_by_field('idnumber', 'x');
3123
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3124
        $this->assertCount(0, $result['courses']);
3125
 
3126
        self::setUser($user1);
3127
        // All visible courses (including front page) for authenticated user.
3128
        $result = core_course_external::get_courses_by_field();
3129
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3130
        $this->assertCount(2, $result['courses']);
3131
        $this->assertCount(34, $result['courses'][0]);  // Site course.
3132
        $this->assertCount(17, $result['courses'][1]);  // Only public information, not enrolled.
3133
 
3134
        $result = core_course_external::get_courses_by_field('id', $course1->id);
3135
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3136
        $this->assertCount(1, $result['courses']);
3137
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3138
        // Expect to receive all the files that a authenticated can see.
3139
        $this->assertCount(17, $result['courses'][0]);
3140
 
3141
        // Course 2 is not visible.
3142
        $result = core_course_external::get_courses_by_field('id', $course2->id);
3143
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3144
        $this->assertCount(0, $result['courses']);
3145
 
3146
        $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
3147
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3148
        $this->assertCount(1, $result['courses']);
3149
 
3150
        $result = core_course_external::get_courses_by_field('category', $category1->id);
3151
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3152
        $this->assertCount(1, $result['courses']);
3153
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3154
 
3155
        $result = core_course_external::get_courses_by_field('shortname', 'c1');
3156
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3157
        $this->assertCount(1, $result['courses']);
3158
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3159
 
3160
        $result = core_course_external::get_courses_by_field('idnumber', 'i2');
3161
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3162
        $this->assertCount(0, $result['courses']);
3163
 
3164
        $result = core_course_external::get_courses_by_field('idnumber', 'x');
3165
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3166
        $this->assertCount(0, $result['courses']);
1441 ariadna 3167
 
3168
        $existingsections = $DB->get_records('course_sections', ['course' => $course1->id]);
3169
        $this->assertEquals(count($existingsections), $numsections + 1); // Includes generic section.
3170
 
3171
        $section = array_shift($existingsections);
3172
        $result = core_course_external::get_courses_by_field('sectionid', $section->id);
3173
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3174
        $this->assertCount(1, $result['courses']);
3175
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3176
 
3177
        // Wrong section.
3178
        $result = core_course_external::get_courses_by_field('sectionid', 1234);
3179
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3180
        $this->assertCount(0, $result['courses']);
1 efrain 3181
    }
3182
 
3183
    /**
3184
     * Test retrieving courses by field returns custom field data
3185
     */
3186
    public function test_get_courses_by_field_customfields(): void {
3187
        $this->resetAfterTest();
3188
        $this->setAdminUser();
3189
 
3190
        $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
3191
        $datefield = $this->getDataGenerator()->create_custom_field([
3192
            'categoryid' => $fieldcategory->get('id'),
3193
            'shortname' => 'mydate',
3194
            'name' => 'My date',
3195
            'type' => 'date',
3196
        ]);
3197
 
3198
        $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
3199
            [
3200
                'shortname' => $datefield->get('shortname'),
3201
                'value' => 1580389200, // 30/01/2020 13:00 GMT.
3202
            ],
3203
        ]]);
3204
 
3205
        $result = external_api::clean_returnvalue(
3206
            core_course_external::get_courses_by_field_returns(),
3207
            core_course_external::get_courses_by_field('id', $newcourse->id)
3208
        );
3209
 
3210
        $this->assertCount(1, $result['courses']);
3211
        $course = reset($result['courses']);
3212
 
3213
        $this->assertArrayHasKey('customfields', $course);
3214
        $this->assertCount(1, $course['customfields']);
3215
 
3216
        // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
3217
        $this->assertEquals([
3218
            'name' => $datefield->get('name'),
3219
            'shortname' => $datefield->get('shortname'),
3220
            'type' => $datefield->get('type'),
3221
            'value' => userdate(1580389200),
3222
            'valueraw' => 1580389200,
3223
        ], reset($course['customfields']));
3224
    }
3225
 
3226
    /**
3227
     * Test retrieving courses by field returning communication tools.
3228
     * @covers \core_course_external::get_courses_by_field
3229
     */
3230
    public function test_get_courses_by_field_communication(): void {
3231
        $this->resetAfterTest();
3232
        $this->setAdminUser();
3233
 
3234
        // Create communication tool in course.
3235
        set_config('enablecommunicationsubsystem', 1);
3236
 
3237
        $roomname = 'Course chat';
3238
        $telegramlink = 'https://my.telegram.chat/120';
3239
        $record = [
3240
            'selectedcommunication' => 'communication_customlink',
3241
            'communicationroomname' => $roomname,
3242
            'customlinkurl' => $telegramlink,
3243
        ];
3244
        $course = $this->getDataGenerator()->create_course($record);
3245
        $communication = \core_communication\api::load_by_instance(
3246
            context: \core\context\course::instance($course->id),
3247
            component: 'core_course',
3248
            instancetype: 'coursecommunication',
3249
            instanceid: $course->id,
3250
        );
3251
 
3252
        $result = external_api::clean_returnvalue(
3253
            core_course_external::get_courses_by_field_returns(),
3254
            core_course_external::get_courses_by_field('id', $course->id)
3255
        );
3256
 
3257
        $course = reset($result['courses']);
3258
        $this->assertEquals($roomname, $course['communicationroomname']);
3259
        $this->assertEquals($telegramlink, $course['communicationroomurl']);
3260
 
3261
        // Course without comm tools.
3262
        $course = $this->getDataGenerator()->create_course();
3263
        $result = external_api::clean_returnvalue(
3264
            core_course_external::get_courses_by_field_returns(),
3265
            core_course_external::get_courses_by_field('id', $course->id)
3266
        );
3267
 
3268
        $course = reset($result['courses']);
3269
        $this->assertNotContains('communicationroomname', $course);
3270
        $this->assertNotContains('communicationroomurl', $course);
3271
    }
3272
 
11 efrain 3273
    public function test_get_courses_by_field_invalid_field(): void {
1 efrain 3274
        $this->expectException('invalid_parameter_exception');
3275
        $result = core_course_external::get_courses_by_field('zyx', 'x');
3276
    }
3277
 
11 efrain 3278
    public function test_get_courses_by_field_invalid_courses(): void {
1 efrain 3279
        $result = core_course_external::get_courses_by_field('id', '-1');
3280
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3281
        $this->assertCount(0, $result['courses']);
3282
    }
3283
 
3284
    /**
3285
     * Test get_courses_by_field_invalid_theme_and_lang
3286
     */
11 efrain 3287
    public function test_get_courses_by_field_invalid_theme_and_lang(): void {
1 efrain 3288
        $this->resetAfterTest(true);
3289
        $this->setAdminUser();
3290
 
3291
        $course = self::getDataGenerator()->create_course(array('theme' => 'kkt', 'lang' => 'kkl'));
3292
        $result = core_course_external::get_courses_by_field('id', $course->id);
3293
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3294
        $this->assertEmpty($result['courses']['0']['theme']);
3295
        $this->assertEmpty($result['courses']['0']['lang']);
3296
    }
3297
 
3298
 
11 efrain 3299
    public function test_check_updates(): void {
1 efrain 3300
        global $DB;
3301
        $this->resetAfterTest(true);
3302
        $this->setAdminUser();
3303
 
3304
        // Create different types of activities.
3305
        $course  = self::getDataGenerator()->create_course();
3306
        $tocreate = [
3307
            'assign',
3308
            'book',
3309
            'choice',
3310
            'folder',
3311
            'forum',
3312
            'glossary',
3313
            'imscp',
3314
            'label',
3315
            'lesson',
3316
            'lti',
3317
            'page',
3318
            'quiz',
3319
            'resource',
3320
            'scorm',
3321
            'url',
3322
            'wiki',
3323
        ];
3324
 
3325
        $modules = array();
3326
        foreach ($tocreate as $modname) {
3327
            $modules[$modname]['instance'] = $this->getDataGenerator()->create_module($modname, array('course' => $course->id));
3328
            $modules[$modname]['cm'] = get_coursemodule_from_id(false, $modules[$modname]['instance']->cmid);
3329
            $modules[$modname]['context'] = context_module::instance($modules[$modname]['instance']->cmid);
3330
        }
3331
 
3332
        $student = self::getDataGenerator()->create_user();
3333
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
3334
        self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
3335
        $this->setUser($student);
3336
 
3337
        $since = time();
3338
        $this->waitForSecond();
3339
        $params = array();
3340
        foreach ($modules as $modname => $data) {
3341
            $params[$data['cm']->id] = array(
3342
                'contextlevel' => 'module',
3343
                'id' => $data['cm']->id,
3344
                'since' => $since
3345
            );
3346
        }
3347
 
3348
        // Check there is nothing updated because modules are fresh new.
3349
        $result = core_course_external::check_updates($course->id, $params);
3350
        $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3351
        $this->assertCount(0, $result['instances']);
3352
        $this->assertCount(0, $result['warnings']);
3353
 
3354
        // Test with get_updates_since the same data.
3355
        $result = core_course_external::get_updates_since($course->id, $since);
3356
        $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
3357
        $this->assertCount(0, $result['instances']);
3358
        $this->assertCount(0, $result['warnings']);
3359
 
3360
        // Update a module after a second.
3361
        $this->waitForSecond();
3362
        set_coursemodule_name($modules['forum']['cm']->id, 'New forum name');
3363
 
3364
        $found = false;
3365
        $result = core_course_external::check_updates($course->id, $params);
3366
        $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3367
        $this->assertCount(1, $result['instances']);
3368
        $this->assertCount(0, $result['warnings']);
3369
        foreach ($result['instances'] as $module) {
3370
            foreach ($module['updates'] as $update) {
3371
                if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
3372
                    $found = true;
3373
                }
3374
            }
3375
        }
3376
        $this->assertTrue($found);
3377
 
3378
        // Test with get_updates_since the same data.
3379
        $result = core_course_external::get_updates_since($course->id, $since);
3380
        $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
3381
        $this->assertCount(1, $result['instances']);
3382
        $this->assertCount(0, $result['warnings']);
3383
        $found = false;
3384
        $this->assertCount(1, $result['instances']);
3385
        $this->assertCount(0, $result['warnings']);
3386
        foreach ($result['instances'] as $module) {
3387
            foreach ($module['updates'] as $update) {
3388
                if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
3389
                    $found = true;
3390
                }
3391
            }
3392
        }
3393
        $this->assertTrue($found);
3394
 
3395
        // Do not retrieve the configuration field.
3396
        $filter = array('files');
3397
        $found = false;
3398
        $result = core_course_external::check_updates($course->id, $params, $filter);
3399
        $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3400
        $this->assertCount(0, $result['instances']);
3401
        $this->assertCount(0, $result['warnings']);
3402
        $this->assertFalse($found);
3403
 
3404
        // Add invalid cmid.
3405
        $params[] = array(
3406
            'contextlevel' => 'module',
3407
            'id' => -2,
3408
            'since' => $since
3409
        );
3410
        $result = core_course_external::check_updates($course->id, $params);
3411
        $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3412
        $this->assertCount(1, $result['warnings']);
3413
        $this->assertEquals(-2, $result['warnings'][0]['itemid']);
3414
    }
3415
 
3416
    /**
3417
     * Test cases for the get_enrolled_courses_by_timeline_classification test.
3418
     */
11 efrain 3419
    public static function get_get_enrolled_courses_by_timeline_classification_test_cases(): array {
1 efrain 3420
        $now = time();
3421
        $day = 86400;
3422
 
3423
        $coursedata = [
3424
            [
3425
                'shortname' => 'apast',
3426
                'startdate' => $now - ($day * 2),
3427
                'enddate' => $now - $day
3428
            ],
3429
            [
3430
                'shortname' => 'bpast',
3431
                'startdate' => $now - ($day * 2),
3432
                'enddate' => $now - $day
3433
            ],
3434
            [
3435
                'shortname' => 'cpast',
3436
                'startdate' => $now - ($day * 2),
3437
                'enddate' => $now - $day
3438
            ],
3439
            [
3440
                'shortname' => 'dpast',
3441
                'startdate' => $now - ($day * 2),
3442
                'enddate' => $now - $day
3443
            ],
3444
            [
3445
                'shortname' => 'epast',
3446
                'startdate' => $now - ($day * 2),
3447
                'enddate' => $now - $day
3448
            ],
3449
            [
3450
                'shortname' => 'ainprogress',
3451
                'startdate' => $now - $day,
3452
                'enddate' => $now + $day
3453
            ],
3454
            [
3455
                'shortname' => 'binprogress',
3456
                'startdate' => $now - $day,
3457
                'enddate' => $now + $day
3458
            ],
3459
            [
3460
                'shortname' => 'cinprogress',
3461
                'startdate' => $now - $day,
3462
                'enddate' => $now + $day
3463
            ],
3464
            [
3465
                'shortname' => 'dinprogress',
3466
                'startdate' => $now - $day,
3467
                'enddate' => $now + $day
3468
            ],
3469
            [
3470
                'shortname' => 'einprogress',
3471
                'startdate' => $now - $day,
3472
                'enddate' => $now + $day
3473
            ],
3474
            [
3475
                'shortname' => 'afuture',
3476
                'startdate' => $now + $day
3477
            ],
3478
            [
3479
                'shortname' => 'bfuture',
3480
                'startdate' => $now + $day
3481
            ],
3482
            [
3483
                'shortname' => 'cfuture',
3484
                'startdate' => $now + $day
3485
            ],
3486
            [
3487
                'shortname' => 'dfuture',
3488
                'startdate' => $now + $day
3489
            ],
3490
            [
3491
                'shortname' => 'efuture',
3492
                'startdate' => $now + $day
3493
            ]
3494
        ];
3495
 
3496
        // Raw enrolled courses result set should be returned in this order:
3497
        // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
3498
        // dfuture, dinprogress, dpast, efuture, einprogress, epast
3499
        //
3500
        // By classification the offset values for each record should be:
3501
        // COURSE_TIMELINE_FUTURE
3502
        // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
3503
        // COURSE_TIMELINE_INPROGRESS
3504
        // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
3505
        // COURSE_TIMELINE_PAST
3506
        // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
3507
        //
3508
        // NOTE: The offset applies to the unfiltered full set of courses before the classification
3509
        // filtering is done.
3510
        // E.g. In our example if an offset of 2 is given then it would mean the first
3511
        // two courses (afuture, ainprogress) are ignored.
3512
        return [
3513
            'empty set' => [
3514
                'coursedata' => [],
3515
                'classification' => 'future',
3516
                'limit' => 2,
3517
                'offset' => 0,
3518
                'sort' => 'shortname ASC',
3519
                'expectedcourses' => [],
3520
                'expectednextoffset' => 0,
3521
            ],
3522
            // COURSE_TIMELINE_FUTURE.
3523
            'future not limit no offset' => [
3524
                'coursedata' => $coursedata,
3525
                'classification' => 'future',
3526
                'limit' => 0,
3527
                'offset' => 0,
3528
                'sort' => 'shortname ASC',
3529
                'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3530
                'expectednextoffset' => 15,
3531
            ],
3532
            'future no offset' => [
3533
                'coursedata' => $coursedata,
3534
                'classification' => 'future',
3535
                'limit' => 2,
3536
                'offset' => 0,
3537
                'sort' => 'shortname ASC',
3538
                'expectedcourses' => ['afuture', 'bfuture'],
3539
                'expectednextoffset' => 4,
3540
            ],
3541
            'future offset' => [
3542
                'coursedata' => $coursedata,
3543
                'classification' => 'future',
3544
                'limit' => 2,
3545
                'offset' => 2,
3546
                'sort' => 'shortname ASC',
3547
                'expectedcourses' => ['bfuture', 'cfuture'],
3548
                'expectednextoffset' => 7,
3549
            ],
3550
            'future exact limit' => [
3551
                'coursedata' => $coursedata,
3552
                'classification' => 'future',
3553
                'limit' => 5,
3554
                'offset' => 0,
3555
                'sort' => 'shortname ASC',
3556
                'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3557
                'expectednextoffset' => 13,
3558
            ],
3559
            'future limit less results' => [
3560
                'coursedata' => $coursedata,
3561
                'classification' => 'future',
3562
                'limit' => 10,
3563
                'offset' => 0,
3564
                'sort' => 'shortname ASC',
3565
                'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3566
                'expectednextoffset' => 15,
3567
            ],
3568
            'future limit less results with offset' => [
3569
                'coursedata' => $coursedata,
3570
                'classification' => 'future',
3571
                'limit' => 10,
3572
                'offset' => 5,
3573
                'sort' => 'shortname ASC',
3574
                'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
3575
                'expectednextoffset' => 15,
3576
            ],
3577
            'all no limit or offset' => [
3578
                'coursedata' => $coursedata,
3579
                'classification' => 'all',
3580
                'limit' => 0,
3581
                'offset' => 0,
3582
                'sort' => 'shortname ASC',
3583
                'expectedcourses' => [
3584
                    'afuture',
3585
                    'ainprogress',
3586
                    'apast',
3587
                    'bfuture',
3588
                    'binprogress',
3589
                    'bpast',
3590
                    'cfuture',
3591
                    'cinprogress',
3592
                    'cpast',
3593
                    'dfuture',
3594
                    'dinprogress',
3595
                    'dpast',
3596
                    'efuture',
3597
                    'einprogress',
3598
                    'epast'
3599
                ],
3600
                'expectednextoffset' => 15,
3601
            ],
3602
            'all limit no offset' => [
3603
                'coursedata' => $coursedata,
3604
                'classification' => 'all',
3605
                'limit' => 5,
3606
                'offset' => 0,
3607
                'sort' => 'shortname ASC',
3608
                'expectedcourses' => [
3609
                    'afuture',
3610
                    'ainprogress',
3611
                    'apast',
3612
                    'bfuture',
3613
                    'binprogress'
3614
                ],
3615
                'expectednextoffset' => 5,
3616
            ],
3617
            'all limit and offset' => [
3618
                'coursedata' => $coursedata,
3619
                'classification' => 'all',
3620
                'limit' => 5,
3621
                'offset' => 5,
3622
                'sort' => 'shortname ASC',
3623
                'expectedcourses' => [
3624
                    'bpast',
3625
                    'cfuture',
3626
                    'cinprogress',
3627
                    'cpast',
3628
                    'dfuture'
3629
                ],
3630
                'expectednextoffset' => 10,
3631
            ],
3632
            'all offset past result set' => [
3633
                'coursedata' => $coursedata,
3634
                'classification' => 'all',
3635
                'limit' => 5,
3636
                'offset' => 50,
3637
                'sort' => 'shortname ASC',
3638
                'expectedcourses' => [],
3639
                'expectednextoffset' => 50,
3640
            ],
3641
            'all limit and offset with sort ul.timeaccess desc' => [
3642
                'coursedata' => $coursedata,
3643
                'classification' => 'inprogress',
3644
                'limit' => 0,
3645
                'offset' => 0,
3646
                'sort' => 'ul.timeaccess desc',
3647
                'expectedcourses' => [
3648
                    'ainprogress',
3649
                    'binprogress',
3650
                    'cinprogress',
3651
                    'dinprogress',
3652
                    'einprogress'
3653
                ],
3654
                'expectednextoffset' => 15,
3655
            ],
3656
            'all limit and offset with sort sql injection for sort or 1==1' => [
3657
                'coursedata' => $coursedata,
3658
                'classification' => 'all',
3659
                'limit' => 5,
3660
                'offset' => 5,
3661
                'sort' => 'ul.timeaccess desc or 1==1',
3662
                'expectedcourses' => [],
3663
                'expectednextoffset' => 0,
3664
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3665
            ],
3666
            'all limit and offset with sql injection of sort a custom one' => [
3667
                'coursedata' => $coursedata,
3668
                'classification' => 'all',
3669
                'limit' => 5,
3670
                'offset' => 5,
3671
                'sort' => "ul.timeaccess LIMIT 1--",
3672
                'expectedcourses' => [],
3673
                'expectednextoffset' => 0,
3674
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3675
            ],
3676
            'all limit and offset with wrong sort direction' => [
3677
                'coursedata' => $coursedata,
3678
                'classification' => 'all',
3679
                'limit' => 5,
3680
                'offset' => 5,
3681
                'sort' => "ul.timeaccess.foo ascd",
3682
                'expectedcourses' => [],
3683
                'expectednextoffset' => 0,
3684
                'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()',
3685
            ],
3686
            'all limit and offset with wrong sort param' => [
3687
                'coursedata' => $coursedata,
3688
                'classification' => 'all',
3689
                'limit' => 5,
3690
                'offset' => 5,
3691
                'sort' => "foobar",
3692
                'expectedcourses' => [],
3693
                'expectednextoffset' => 0,
3694
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3695
            ],
3696
            'all limit and offset with wrong field separator' => [
3697
                'coursedata' => $coursedata,
3698
                'classification' => 'all',
3699
                'limit' => 5,
3700
                'offset' => 5,
3701
                'sort' => "ul.timeaccess.foo",
3702
                'expectedcourses' => [],
3703
                'expectednextoffset' => 0,
3704
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3705
            ],
3706
            'all limit and offset with wrong field separator #' => [
3707
                'coursedata' => $coursedata,
3708
                'classification' => 'all',
3709
                'limit' => 5,
3710
                'offset' => 5,
3711
                'sort' => "ul#timeaccess",
3712
                'expectedcourses' => [],
3713
                'expectednextoffset' => 0,
3714
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3715
            ],
3716
            'all limit and offset with wrong field separator $' => [
3717
                'coursedata' => $coursedata,
3718
                'classification' => 'all',
3719
                'limit' => 5,
3720
                'offset' => 5,
3721
                'sort' => 'ul$timeaccess',
3722
                'expectedcourses' => [],
3723
                'expectednextoffset' => 0,
3724
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3725
            ],
3726
            'all limit and offset with wrong field name' => [
3727
                'coursedata' => $coursedata,
3728
                'classification' => 'all',
3729
                'limit' => 5,
3730
                'offset' => 5,
3731
                'sort' => 'timeaccess123',
3732
                'expectedcourses' => [],
3733
                'expectednextoffset' => 0,
3734
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3735
            ],
3736
            'all limit and offset with no sort direction for ul' => [
3737
                'coursedata' => $coursedata,
3738
                'classification' => 'inprogress',
3739
                'limit' => 0,
3740
                'offset' => 0,
3741
                'sort' => "ul.timeaccess",
3742
                'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3743
                'expectednextoffset' => 15,
3744
            ],
3745
            'all limit and offset with valid field name and no prefix, test for ul' => [
3746
                'coursedata' => $coursedata,
3747
                'classification' => 'inprogress',
3748
                'limit' => 0,
3749
                'offset' => 0,
3750
                'sort' => "timeaccess",
3751
                'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3752
                'expectednextoffset' => 15,
3753
            ],
3754
            'all limit and offset with valid field name and no prefix' => [
3755
                'coursedata' => $coursedata,
3756
                'classification' => 'all',
3757
                'limit' => 5,
3758
                'offset' => 5,
3759
                'sort' => "fullname",
3760
                'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
3761
                'expectednextoffset' => 10,
3762
            ],
3763
            'all limit and offset with valid field name and no prefix and with sort direction' => [
3764
                'coursedata' => $coursedata,
3765
                'classification' => 'all',
3766
                'limit' => 5,
3767
                'offset' => 5,
3768
                'sort' => "fullname desc",
3769
                'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
3770
                'expectednextoffset' => 10,
3771
            ],
3772
            'Search courses for courses containing bfut' => [
3773
                'coursedata' => $coursedata,
3774
                'classification' => 'search',
3775
                'limit' => 0,
3776
                'offset' => 0,
3777
                'sort' => null,
3778
                'expectedcourses' => ['bfuture'],
3779
                'expectednextoffset' => 1,
3780
                'expectedexception' => null,
3781
                'searchvalue' => 'bfut',
3782
            ],
3783
            'Search courses for courses containing inp' => [
3784
                'coursedata' => $coursedata,
3785
                'classification' => 'search',
3786
                'limit' => 0,
3787
                'offset' => 0,
3788
                'sort' => null,
3789
                'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3790
                'expectednextoffset' => 5,
3791
                'expectedexception' => null,
3792
                'searchvalue' => 'inp',
3793
            ],
3794
            'Search courses for courses containing fail' => [
3795
                'coursedata' => $coursedata,
3796
                'classification' => 'search',
3797
                'limit' => 0,
3798
                'offset' => 0,
3799
                'sort' => null,
3800
                'expectedcourses' => [],
3801
                'expectednextoffset' => 0,
3802
                'expectedexception' => null,
3803
                'searchvalue' => 'fail',
3804
            ],
3805
            'Search courses for courses containing !`~[]C' => [
3806
                'coursedata' => $coursedata,
3807
                'classification' => 'search',
3808
                'limit' => 0,
3809
                'offset' => 0,
3810
                'sort' => null,
3811
                'expectedcourses' => [],
3812
                'expectednextoffset' => 0,
3813
                'expectedexception' => null,
3814
                'searchvalue' => '!`~[]C',
3815
            ],
3816
        ];
3817
    }
3818
 
3819
    /**
3820
     * Test the get_enrolled_courses_by_timeline_classification function.
3821
     *
1441 ariadna 3822
     * @dataProvider get_get_enrolled_courses_by_timeline_classification_test_cases
1 efrain 3823
     * @param array $coursedata Courses to create
3824
     * @param string $classification Timeline classification
3825
     * @param int $limit Maximum number of results
3826
     * @param int $offset Offset the unfiltered courses result set by this amount
3827
     * @param string $sort sort the courses
3828
     * @param array $expectedcourses Expected courses in result
3829
     * @param int $expectednextoffset Expected next offset value in result
3830
     * @param string|null $expectedexception Expected exception string
3831
     * @param string|null $searchvalue If we are searching, what do we need to look for?
3832
     */
3833
    public function test_get_enrolled_courses_by_timeline_classification(
3834
        $coursedata,
3835
        $classification,
3836
        $limit,
3837
        $offset,
3838
        $sort,
3839
        $expectedcourses,
3840
        $expectednextoffset,
3841
        $expectedexception = null,
3842
        $searchvalue = null
11 efrain 3843
    ): void {
1 efrain 3844
        $this->resetAfterTest();
3845
        $generator = $this->getDataGenerator();
3846
 
3847
        $courses = array_map(function($coursedata) use ($generator) {
3848
            return $generator->create_course($coursedata);
3849
        }, $coursedata);
3850
 
3851
        $student = $generator->create_user();
3852
 
3853
        foreach ($courses as $course) {
3854
            $generator->enrol_user($student->id, $course->id, 'student');
3855
        }
3856
 
3857
        $this->setUser($student);
3858
 
3859
        if (isset($expectedexception)) {
3860
            $this->expectException('coding_exception');
3861
            $this->expectExceptionMessage($expectedexception);
3862
        }
3863
 
3864
        // NOTE: The offset applies to the unfiltered full set of courses before the classification
3865
        // filtering is done.
3866
        // E.g. In our example if an offset of 2 is given then it would mean the first
3867
        // two courses (afuture, ainprogress) are ignored.
3868
        $result = core_course_external::get_enrolled_courses_by_timeline_classification(
3869
            $classification,
3870
            $limit,
3871
            $offset,
3872
            $sort,
3873
            null,
3874
            null,
3875
            $searchvalue
3876
        );
3877
        $result = external_api::clean_returnvalue(
3878
            core_course_external::get_enrolled_courses_by_timeline_classification_returns(),
3879
            $result
3880
        );
3881
 
3882
        $actual = array_map(function($course) {
3883
            return $course['shortname'];
3884
        }, $result['courses']);
3885
 
3886
        $this->assertEqualsCanonicalizing($expectedcourses, $actual);
3887
        $this->assertEquals($expectednextoffset, $result['nextoffset']);
3888
    }
3889
 
3890
    /**
3891
     * Test the get_recent_courses function.
3892
     */
11 efrain 3893
    public function test_get_recent_courses(): void {
1 efrain 3894
        global $USER, $DB;
3895
 
3896
        $this->resetAfterTest();
3897
        $generator = $this->getDataGenerator();
3898
 
3899
        set_config('hiddenuserfields', 'lastaccess');
3900
 
3901
        $courses = array();
3902
        for ($i = 1; $i < 12; $i++) {
3903
            $courses[]  = $generator->create_course();
3904
        };
3905
 
3906
        $student = $generator->create_user();
3907
        $teacher = $generator->create_user();
3908
 
3909
        foreach ($courses as $course) {
3910
            $generator->enrol_user($student->id, $course->id, 'student');
3911
        }
3912
 
3913
        $generator->enrol_user($teacher->id, $courses[0]->id, 'teacher');
3914
 
3915
        $this->setUser($student);
3916
 
3917
        $result = core_course_external::get_recent_courses($USER->id);
3918
 
3919
        // No course accessed.
3920
        $this->assertCount(0, $result);
3921
 
3922
        foreach ($courses as $course) {
3923
            core_course_external::view_course($course->id);
3924
        }
3925
 
3926
        // Every course accessed.
3927
        $result = core_course_external::get_recent_courses($USER->id);
3928
        $this->assertCount( 11, $result);
3929
 
3930
        // Every course accessed, result limited to 10 courses.
3931
        $result = core_course_external::get_recent_courses($USER->id, 10);
3932
        $this->assertCount(10, $result);
3933
 
3934
        $guestcourse = $generator->create_course(
3935
                (object)array('shortname' => 'guestcourse',
3936
                'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
3937
                'enrol_guest_password_0' => ''));
3938
        core_course_external::view_course($guestcourse->id);
3939
 
3940
        // Every course accessed, even the not enrolled one.
3941
        $result = core_course_external::get_recent_courses($USER->id);
3942
        $this->assertCount(12, $result);
3943
 
3944
        // Offset 5, return 7 out of 12.
3945
        $result = core_course_external::get_recent_courses($USER->id, 0, 5);
3946
        $this->assertCount(7, $result);
3947
 
3948
        // Offset 5 and limit 3, return 3 out of 12.
3949
        $result = core_course_external::get_recent_courses($USER->id, 3, 5);
3950
        $this->assertCount(3, $result);
3951
 
3952
        // Sorted by course id ASC.
3953
        $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id ASC');
3954
        $this->assertEquals($courses[0]->id, array_shift($result)->id);
3955
 
3956
        // Sorted by course id DESC.
3957
        $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id DESC');
3958
        $this->assertEquals($guestcourse->id, array_shift($result)->id);
3959
 
3960
        // If last access is hidden, only get the courses where has viewhiddenuserfields capability.
3961
        $this->setUser($teacher);
3962
        $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
3963
        $usercontext = context_user::instance($student->id);
3964
        $this->assignUserCapability('moodle/user:viewdetails', $usercontext, $teacherroleid);
3965
 
3966
        // Sorted by course id DESC.
1441 ariadna 3967
        // User without moodle/user:viewalldetails capability will not be able to see the course details.
1 efrain 3968
        $result = core_course_external::get_recent_courses($student->id);
1441 ariadna 3969
        $this->assertCount(0, $result);
3970
 
3971
        // User with moodle/user:viewalldetails capability will be able to see the course details.
3972
        $this->assignUserCapability('moodle/user:viewalldetails', $usercontext, $teacherroleid);
3973
        $result = core_course_external::get_recent_courses($student->id);
1 efrain 3974
        $this->assertCount(1, $result);
3975
        $this->assertEquals($courses[0]->id, array_shift($result)->id);
3976
    }
3977
 
3978
    /**
3979
     * Test get enrolled users by cmid function.
3980
     */
11 efrain 3981
    public function test_get_enrolled_users_by_cmid(): void {
1 efrain 3982
        global $PAGE;
3983
        $this->resetAfterTest(true);
3984
 
3985
        $user1 = self::getDataGenerator()->create_user();
3986
        $user2 = self::getDataGenerator()->create_user();
3987
        $user3 = self::getDataGenerator()->create_user();
3988
 
3989
        $user1picture = new user_picture($user1);
3990
        $user1picture->size = 1;
3991
        $user1->profileimage = $user1picture->get_url($PAGE)->out(false);
3992
 
3993
        $user2picture = new user_picture($user2);
3994
        $user2picture->size = 1;
3995
        $user2->profileimage = $user2picture->get_url($PAGE)->out(false);
3996
 
3997
        $user3picture = new user_picture($user3);
3998
        $user3picture->size = 1;
3999
        $user3->profileimage = $user3picture->get_url($PAGE)->out(false);
4000
 
4001
        // Set the first created user to the test user.
4002
        self::setUser($user1);
4003
 
4004
        // Create course to add the module.
4005
        $course1 = self::getDataGenerator()->create_course();
4006
 
4007
        // Forum with tracking off.
4008
        $record = new stdClass();
4009
        $record->course = $course1->id;
4010
        $forum1 = self::getDataGenerator()->create_module('forum', $record);
4011
 
4012
        // Following lines enrol and assign default role id to the users.
4013
        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
4014
        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
4015
        // Enrol a suspended user in the course.
4016
        $this->getDataGenerator()->enrol_user($user3->id, $course1->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
4017
 
4018
        // Create what we expect to be returned when querying the course module.
4019
        $expectedusers = array(
4020
            'users' => array(),
4021
            'warnings' => array(),
4022
        );
4023
 
4024
        $expectedusers['users'][0] = [
4025
            'id' => $user1->id,
4026
            'fullname' => fullname($user1),
4027
            'firstname' => $user1->firstname,
4028
            'lastname' => $user1->lastname,
4029
            'profileimage' => $user1->profileimage,
4030
        ];
4031
        $expectedusers['users'][1] = [
4032
            'id' => $user2->id,
4033
            'fullname' => fullname($user2),
4034
            'firstname' => $user2->firstname,
4035
            'lastname' => $user2->lastname,
4036
            'profileimage' => $user2->profileimage,
4037
        ];
4038
        $expectedusers['users'][2] = [
4039
            'id' => $user3->id,
4040
            'fullname' => fullname($user3),
4041
            'firstname' => $user3->firstname,
4042
            'lastname' => $user3->lastname,
4043
            'profileimage' => $user3->profileimage,
4044
        ];
4045
 
4046
        // Test getting the users in a given context.
4047
        $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid);
4048
        $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users);
4049
 
4050
        $this->assertEquals(3, count($users['users']));
4051
        $this->assertEquals($expectedusers, $users);
4052
 
4053
        // Test getting only the active users in a given context.
4054
        $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid, 0, true);
4055
        $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users);
4056
 
4057
        $expectedusers['users'] = [
4058
            [
4059
                'id' => $user1->id,
4060
                'fullname' => fullname($user1),
4061
                'firstname' => $user1->firstname,
4062
                'lastname' => $user1->lastname,
4063
                'profileimage' => $user1->profileimage,
4064
            ],
4065
            [
4066
                'id' => $user2->id,
4067
                'fullname' => fullname($user2),
4068
                'firstname' => $user2->firstname,
4069
                'lastname' => $user2->lastname,
4070
                'profileimage' => $user2->profileimage,
4071
            ]
4072
        ];
4073
 
4074
        $this->assertEquals(2, count($users['users']));
4075
        $this->assertEquals($expectedusers, $users);
1441 ariadna 4076
 
4077
        // Prohibit the capability for viewing course participants.
4078
        $this->unassignUserCapability('moodle/course:viewparticipants', null, null, $course1->id);
4079
        $this->expectException(required_capability_exception::class);
4080
        $this->expectExceptionMessage('Sorry, but you do not currently have permissions to do that (View participants)');
4081
        core_course_external::get_enrolled_users_by_cmid($forum1->cmid);
1 efrain 4082
    }
4083
 
4084
    /**
4085
     * Verify that content items can be added to user favourites.
4086
     */
11 efrain 4087
    public function test_add_content_item_to_user_favourites(): void {
1 efrain 4088
        $this->resetAfterTest();
4089
 
4090
        $course = $this->getDataGenerator()->create_course();
4091
        $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
4092
        $this->setUser($user);
4093
 
4094
        // Using the internal API, confirm that no items are set as favourites for the user.
4095
        $contentitemservice = new \core_course\local\service\content_item_service(
4096
            new \core_course\local\repository\content_item_readonly_repository()
4097
        );
4098
        $contentitems = $contentitemservice->get_all_content_items($user);
4099
        $favourited = array_filter($contentitems, function($contentitem) {
4100
            return $contentitem->favourite == true;
4101
        });
4102
        $this->assertCount(0, $favourited);
4103
 
4104
        // Using the external API, favourite a content item for the user.
4105
        $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))];
4106
        $contentitem = core_course_external::add_content_item_to_user_favourites('mod_assign', $assign->id, $user->id);
4107
        $contentitem = external_api::clean_returnvalue(core_course_external::add_content_item_to_user_favourites_returns(),
4108
            $contentitem);
4109
 
4110
        // Verify the returned item is a favourite.
4111
        $this->assertTrue($contentitem['favourite']);
4112
 
4113
        // Using the internal API, confirm we see a single favourite item.
4114
        $contentitems = $contentitemservice->get_all_content_items($user);
4115
        $favourited = array_values(array_filter($contentitems, function($contentitem) {
4116
            return $contentitem->favourite == true;
4117
        }));
4118
        $this->assertCount(1, $favourited);
4119
        $this->assertEquals('assign', $favourited[0]->name);
4120
    }
4121
 
4122
    /**
4123
     * Verify that content items can be removed from user favourites.
4124
     */
11 efrain 4125
    public function test_remove_content_item_from_user_favourites(): void {
1 efrain 4126
        $this->resetAfterTest();
4127
 
4128
        $course = $this->getDataGenerator()->create_course();
4129
        $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
4130
        $this->setUser($user);
4131
 
4132
        // Using the internal API, set a favourite for the user.
4133
        $contentitemservice = new \core_course\local\service\content_item_service(
4134
            new \core_course\local\repository\content_item_readonly_repository()
4135
        );
4136
        $contentitems = $contentitemservice->get_all_content_items($user);
4137
        $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))];
4138
        $contentitemservice->add_to_user_favourites($user, $assign->componentname, $assign->id);
4139
 
4140
        $contentitems = $contentitemservice->get_all_content_items($user);
4141
        $favourited = array_filter($contentitems, function($contentitem) {
4142
            return $contentitem->favourite == true;
4143
        });
4144
        $this->assertCount(1, $favourited);
4145
 
4146
        // Now, verify the external API can remove the favourite.
4147
        $contentitem = core_course_external::remove_content_item_from_user_favourites('mod_assign', $assign->id);
4148
        $contentitem = external_api::clean_returnvalue(core_course_external::remove_content_item_from_user_favourites_returns(),
4149
            $contentitem);
4150
 
4151
        // Verify the returned item is a favourite.
4152
        $this->assertFalse($contentitem['favourite']);
4153
 
4154
        // Using the internal API, confirm we see no favourite items.
4155
        $contentitems = $contentitemservice->get_all_content_items($user);
4156
        $favourited = array_filter($contentitems, function($contentitem) {
4157
            return $contentitem->favourite == true;
4158
        });
4159
        $this->assertCount(0, $favourited);
4160
    }
4161
 
4162
    /**
4163
     * Test the web service returning course content items for inclusion in activity choosers, etc.
4164
     */
11 efrain 4165
    public function test_get_course_content_items(): void {
1 efrain 4166
        $this->resetAfterTest();
4167
 
4168
        $course  = self::getDataGenerator()->create_course();
4169
        $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
4170
 
4171
        // Fetch available content items as the editing teacher.
4172
        $this->setUser($user);
4173
        $result = core_course_external::get_course_content_items($course->id);
4174
        $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result);
4175
 
4176
        $contentitemservice = new \core_course\local\service\content_item_service(
4177
            new \core_course\local\repository\content_item_readonly_repository()
4178
        );
4179
 
4180
        // Check if the webservice returns exactly what the service defines, albeit in array form.
4181
        $serviceitemsasarray = array_map(function($item) {
4182
            return (array) $item;
4183
        }, $contentitemservice->get_content_items_for_user_in_course($user, $course));
4184
 
4185
        $this->assertEquals($serviceitemsasarray, $result['content_items']);
4186
    }
4187
 
4188
    /**
4189
     * Test the web service returning course content items, specifically in case where the user can't manage activities.
4190
     */
11 efrain 4191
    public function test_get_course_content_items_no_permission_to_manage(): void {
1 efrain 4192
        $this->resetAfterTest();
4193
 
4194
        $course  = self::getDataGenerator()->create_course();
4195
        $user = self::getDataGenerator()->create_and_enrol($course, 'student');
4196
 
4197
        // Fetch available content items as a student, who won't have the permission to manage activities.
4198
        $this->setUser($user);
4199
        $result = core_course_external::get_course_content_items($course->id);
4200
        $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result);
4201
 
4202
        $this->assertEmpty($result['content_items']);
4203
    }
4204
 
4205
    /**
4206
     * Test toggling the recommendation of an activity.
4207
     */
11 efrain 4208
    public function test_toggle_activity_recommendation(): void {
1 efrain 4209
        global $CFG;
4210
 
4211
        $this->resetAfterTest();
4212
 
4213
        $context = context_system::instance();
4214
        $usercontext = context_user::instance($CFG->siteguest);
4215
        $component = 'core_course';
4216
        $favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext);
4217
 
4218
        $areaname = 'test_core';
4219
        $areaid = 3;
4220
 
4221
        // Test we have the favourite.
4222
        $this->setAdminUser();
4223
        $result = core_course_external::toggle_activity_recommendation($areaname, $areaid);
4224
        $this->assertTrue($favouritefactory->favourite_exists($component,
4225
                \core_course\local\service\content_item_service::RECOMMENDATION_PREFIX . $areaname, $areaid, $context));
4226
        $this->assertTrue($result['status']);
4227
        // Test that it is now gone.
4228
        $result = core_course_external::toggle_activity_recommendation($areaname, $areaid);
4229
        $this->assertFalse($favouritefactory->favourite_exists($component, $areaname, $areaid, $context));
4230
        $this->assertFalse($result['status']);
4231
    }
4232
 
4233
}