Proyectos de Subversion Moodle

Rev

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

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core_course;
18
 
19
use advanced_testcase;
20
use backup_controller;
21
use backup;
22
use blog_entry;
23
use cache;
24
use calendar_event;
25
use coding_exception;
26
use comment;
27
use completion_criteria_date;
28
use completion_completion;
29
use context_course;
30
use context_module;
31
use context_system;
32
use context_coursecat;
33
use core\event\section_viewed;
34
use core_completion_external;
35
use core_external;
36
use core_tag_index_builder;
37
use core_tag_tag;
38
use course_capability_assignment;
39
use course_request;
40
use core_course_category;
41
use enrol_imsenterprise\imsenterprise_test;
42
use core_external\external_api;
43
use grade_item;
44
use grading_manager;
45
use moodle_exception;
46
use moodle_url;
47
use phpunit_util;
48
use rating_manager;
49
use restore_controller;
50
use stdClass;
51
use testing_data_generator;
52
 
53
defined('MOODLE_INTERNAL') or die();
54
 
55
// Require library globally because it's constants are used within dataProvider methods, executed before setUpBeforeClass.
56
global $CFG;
57
require_once($CFG->dirroot . '/course/lib.php');
58
 
59
/**
60
 * Course related unit tests
61
 *
62
 * @package    core_course
63
 * @category   test
64
 * @copyright  2012 Petr Skoda {@link http://skodak.org}
65
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
66
 */
67
class courselib_test extends advanced_testcase {
68
 
69
    /**
70
     * Load required libraries and fixtures.
71
     */
72
    public static function setUpBeforeClass(): void {
73
        global $CFG;
74
 
75
        require_once($CFG->dirroot . '/course/tests/fixtures/course_capability_assignment.php');
76
        require_once($CFG->dirroot . '/enrol/imsenterprise/tests/imsenterprise_test.php');
77
    }
78
 
79
    /**
80
     * Set forum specific test values for calling create_module().
81
     *
82
     * @param object $moduleinfo - the moduleinfo to add some specific values - passed in reference.
83
     */
84
    private function forum_create_set_values(&$moduleinfo) {
85
        // Completion specific to forum - optional.
86
        $moduleinfo->completionposts = 3;
87
        $moduleinfo->completiondiscussions = 1;
88
        $moduleinfo->completionreplies = 2;
89
 
90
        // Specific values to the Forum module.
91
        $moduleinfo->forcesubscribe = FORUM_INITIALSUBSCRIBE;
92
        $moduleinfo->type = 'single';
93
        $moduleinfo->trackingtype = FORUM_TRACKING_FORCED;
94
        $moduleinfo->maxbytes = 10240;
95
        $moduleinfo->maxattachments = 2;
96
 
97
        // Post threshold for blocking - specific to forum.
98
        $moduleinfo->blockperiod = 60*60*24;
99
        $moduleinfo->blockafter = 10;
100
        $moduleinfo->warnafter = 5;
101
 
102
        // Grading of whole forum settings.
103
        $moduleinfo->grade_forum = 0;
104
    }
105
 
106
    /**
107
     * Execute test asserts on the saved DB data by create_module($forum).
108
     *
109
     * @param object $moduleinfo - the specific forum values that were used to create a forum.
110
     * @param object $dbmodinstance - the DB values of the created forum.
111
     */
112
    private function forum_create_run_asserts($moduleinfo, $dbmodinstance) {
113
        // Compare values specific to forums.
114
        $this->assertEquals($moduleinfo->forcesubscribe, $dbmodinstance->forcesubscribe);
115
        $this->assertEquals($moduleinfo->type, $dbmodinstance->type);
116
        $this->assertEquals($moduleinfo->assessed, $dbmodinstance->assessed);
117
        $this->assertEquals($moduleinfo->completionposts, $dbmodinstance->completionposts);
118
        $this->assertEquals($moduleinfo->completiondiscussions, $dbmodinstance->completiondiscussions);
119
        $this->assertEquals($moduleinfo->completionreplies, $dbmodinstance->completionreplies);
120
        $this->assertEquals($moduleinfo->scale, $dbmodinstance->scale);
121
        $this->assertEquals($moduleinfo->assesstimestart, $dbmodinstance->assesstimestart);
122
        $this->assertEquals($moduleinfo->assesstimefinish, $dbmodinstance->assesstimefinish);
123
        $this->assertEquals($moduleinfo->rsstype, $dbmodinstance->rsstype);
124
        $this->assertEquals($moduleinfo->rssarticles, $dbmodinstance->rssarticles);
125
        $this->assertEquals($moduleinfo->trackingtype, $dbmodinstance->trackingtype);
126
        $this->assertEquals($moduleinfo->maxbytes, $dbmodinstance->maxbytes);
127
        $this->assertEquals($moduleinfo->maxattachments, $dbmodinstance->maxattachments);
128
        $this->assertEquals($moduleinfo->blockperiod, $dbmodinstance->blockperiod);
129
        $this->assertEquals($moduleinfo->blockafter, $dbmodinstance->blockafter);
130
        $this->assertEquals($moduleinfo->warnafter, $dbmodinstance->warnafter);
131
    }
132
 
133
    /**
134
     * Set assign module specific test values for calling create_module().
135
     *
136
     * @param object $moduleinfo - the moduleinfo to add some specific values - passed in reference.
137
     */
138
    private function assign_create_set_values(&$moduleinfo) {
139
        // Specific values to the Assign module.
140
        $moduleinfo->alwaysshowdescription = true;
141
        $moduleinfo->submissiondrafts = true;
142
        $moduleinfo->requiresubmissionstatement = true;
143
        $moduleinfo->sendnotifications = true;
144
        $moduleinfo->sendlatenotifications = true;
145
        $moduleinfo->duedate = time() + (7 * 24 * 3600);
146
        $moduleinfo->cutoffdate = time() + (7 * 24 * 3600);
147
        $moduleinfo->gradingduedate = time() + (7 * 24 * 3600);
148
        $moduleinfo->allowsubmissionsfromdate = time();
149
        $moduleinfo->teamsubmission = true;
150
        $moduleinfo->requireallteammemberssubmit = true;
151
        $moduleinfo->teamsubmissiongroupingid = true;
152
        $moduleinfo->blindmarking = true;
153
        $moduleinfo->markingworkflow = true;
154
        $moduleinfo->markingallocation = true;
155
        $moduleinfo->assignsubmission_onlinetext_enabled = true;
156
        $moduleinfo->assignsubmission_file_enabled = true;
157
        $moduleinfo->assignsubmission_file_maxfiles = 1;
158
        $moduleinfo->assignsubmission_file_maxsizebytes = 1000000;
159
        $moduleinfo->assignsubmission_comments_enabled = true;
160
        $moduleinfo->assignfeedback_comments_enabled = true;
161
        $moduleinfo->assignfeedback_offline_enabled = true;
162
        $moduleinfo->assignfeedback_file_enabled = true;
163
 
164
        // Advanced grading.
165
        $gradingmethods = grading_manager::available_methods();
166
        $moduleinfo->advancedgradingmethod_submissions = current(array_keys($gradingmethods));
167
    }
168
 
169
    /**
170
     * Execute test asserts on the saved DB data by create_module($assign).
171
     *
172
     * @param object $moduleinfo - the specific assign module values that were used to create an assign module.
173
     * @param object $dbmodinstance - the DB values of the created assign module.
174
     */
175
    private function assign_create_run_asserts($moduleinfo, $dbmodinstance) {
176
        global $DB;
177
 
178
        $this->assertEquals($moduleinfo->alwaysshowdescription, $dbmodinstance->alwaysshowdescription);
179
        $this->assertEquals($moduleinfo->submissiondrafts, $dbmodinstance->submissiondrafts);
180
        $this->assertEquals($moduleinfo->requiresubmissionstatement, $dbmodinstance->requiresubmissionstatement);
181
        $this->assertEquals($moduleinfo->sendnotifications, $dbmodinstance->sendnotifications);
182
        $this->assertEquals($moduleinfo->duedate, $dbmodinstance->duedate);
183
        $this->assertEquals($moduleinfo->cutoffdate, $dbmodinstance->cutoffdate);
184
        $this->assertEquals($moduleinfo->allowsubmissionsfromdate, $dbmodinstance->allowsubmissionsfromdate);
185
        $this->assertEquals($moduleinfo->teamsubmission, $dbmodinstance->teamsubmission);
186
        $this->assertEquals($moduleinfo->requireallteammemberssubmit, $dbmodinstance->requireallteammemberssubmit);
187
        $this->assertEquals($moduleinfo->teamsubmissiongroupingid, $dbmodinstance->teamsubmissiongroupingid);
188
        $this->assertEquals($moduleinfo->blindmarking, $dbmodinstance->blindmarking);
189
        $this->assertEquals($moduleinfo->markingworkflow, $dbmodinstance->markingworkflow);
190
        $this->assertEquals($moduleinfo->markingallocation, $dbmodinstance->markingallocation);
191
        // The goal not being to fully test assign_add_instance() we'll stop here for the assign tests - to avoid too many DB queries.
192
 
193
        // Advanced grading.
194
        $cm = get_coursemodule_from_instance('assign', $dbmodinstance->id);
195
        $contextmodule = context_module::instance($cm->id);
196
        $advancedgradingmethod = $DB->get_record('grading_areas',
197
            array('contextid' => $contextmodule->id,
198
                'activemethod' => $moduleinfo->advancedgradingmethod_submissions));
199
        $this->assertEquals($moduleinfo->advancedgradingmethod_submissions, $advancedgradingmethod);
200
    }
201
 
202
    /**
203
     * Run some asserts test for a specific module for the function create_module().
204
     *
205
     * The function has been created (and is called) for $this->test_create_module().
206
     * Note that the call to MODULE_create_set_values and MODULE_create_run_asserts are done after the common set values/run asserts.
207
     * So if you want, you can overwrite the default values/asserts in the respective functions.
208
     * @param string $modulename Name of the module ('forum', 'assign', 'book'...).
209
     */
210
    private function create_specific_module_test($modulename) {
211
        global $DB, $CFG;
212
 
213
        $this->resetAfterTest(true);
214
 
215
        $this->setAdminUser();
216
 
217
        // Warnings: you'll need to change this line if ever you come to test a module not following Moodle standard.
218
        require_once($CFG->dirroot.'/mod/'. $modulename .'/lib.php');
219
 
220
        // Enable avaibility.
221
        // If not enabled all conditional fields will be ignored.
222
        set_config('enableavailability', 1);
223
 
224
        // Enable course completion.
225
        // If not enabled all completion settings will be ignored.
226
        set_config('enablecompletion', COMPLETION_ENABLED);
227
 
228
        // Enable forum RSS feeds.
229
        set_config('enablerssfeeds', 1);
230
        set_config('forum_enablerssfeeds', 1);
231
 
232
        $course = $this->getDataGenerator()->create_course(array('numsections'=>1, 'enablecompletion' => COMPLETION_ENABLED),
233
           array('createsections'=>true));
234
 
235
        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
236
 
237
        // Create assign module instance for test.
238
        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
239
        $params['course'] = $course->id;
240
        $instance = $generator->create_instance($params);
241
        $assigncm = get_coursemodule_from_instance('assign', $instance->id);
242
 
243
        // Module test values.
244
        $moduleinfo = new stdClass();
245
 
246
        // Always mandatory generic values to any module.
247
        $moduleinfo->modulename = $modulename;
248
        $moduleinfo->section = 1; // This is the section number in the course. Not the section id in the database.
249
        $moduleinfo->course = $course->id;
250
        $moduleinfo->groupingid = $grouping->id;
251
        $moduleinfo->visible = true;
252
        $moduleinfo->visibleoncoursepage = true;
253
 
254
        // Sometimes optional generic values for some modules.
255
        $moduleinfo->name = 'My test module';
256
        $moduleinfo->showdescription = 1; // standard boolean
257
        require_once($CFG->libdir . '/gradelib.php');
258
        $gradecats = grade_get_categories_menu($moduleinfo->course, false);
259
        $gradecatid = current(array_keys($gradecats)); // Retrieve the first key of $gradecats
260
        $moduleinfo->gradecat = $gradecatid;
261
        $moduleinfo->groupmode = VISIBLEGROUPS;
262
        $moduleinfo->cmidnumber = 'idnumber_XXX';
263
 
264
        // Completion common to all module.
265
        $moduleinfo->completion = COMPLETION_TRACKING_AUTOMATIC;
266
        $moduleinfo->completionview = COMPLETION_VIEW_REQUIRED;
267
        $moduleinfo->completiongradeitemnumber = 1;
268
        $moduleinfo->completionpassgrade = 0;
269
        $moduleinfo->completionexpected = time() + (7 * 24 * 3600);
270
 
271
        // Conditional activity.
272
        $moduleinfo->availability = '{"op":"&","showc":[true,true],"c":[' .
273
                '{"type":"date","d":">=","t":' . time() . '},' .
274
                '{"type":"date","d":"<","t":' . (time() + (7 * 24 * 3600)) . '}' .
275
                ']}';
276
        $coursegradeitem = grade_item::fetch_course_item($moduleinfo->course); //the activity will become available only when the user reach some grade into the course itself.
277
        $moduleinfo->conditiongradegroup = array(array('conditiongradeitemid' => $coursegradeitem->id, 'conditiongrademin' => 10, 'conditiongrademax' => 80));
278
        $moduleinfo->conditionfieldgroup = array(array('conditionfield' => 'email', 'conditionfieldoperator' => \availability_profile\condition::OP_CONTAINS, 'conditionfieldvalue' => '@'));
279
        $moduleinfo->conditioncompletiongroup = array(array('conditionsourcecmid' => $assigncm->id, 'conditionrequiredcompletion' => COMPLETION_COMPLETE)); // "conditionsourcecmid == 0" => none
280
 
281
        // Grading and Advanced grading.
282
        require_once($CFG->dirroot . '/rating/lib.php');
283
        $moduleinfo->assessed = RATING_AGGREGATE_AVERAGE;
284
        $moduleinfo->scale = 10; // Note: it could be minus (for specific course scale). It is a signed number.
285
        $moduleinfo->assesstimestart = time();
286
        $moduleinfo->assesstimefinish = time() + (7 * 24 * 3600);
287
 
288
        // RSS.
289
        $moduleinfo->rsstype = 2;
290
        $moduleinfo->rssarticles = 10;
291
 
292
        // Optional intro editor (depends of module).
293
        $draftid_editor = 0;
294
        file_prepare_draft_area($draftid_editor, null, null, null, null);
295
        $moduleinfo->introeditor = array('text' => 'This is a module', 'format' => FORMAT_HTML, 'itemid' => $draftid_editor);
296
 
297
        // Following is the advanced grading method area called 'submissions' for the 'assign' module.
298
        if (plugin_supports('mod', $modulename, FEATURE_GRADE_HAS_GRADE, false) && !plugin_supports('mod', $modulename, FEATURE_RATE, false)) {
299
            $moduleinfo->grade = 100;
300
        }
301
 
302
        // Plagiarism form values.
303
        // No plagiarism plugin installed by default. Use this space to make your own test.
304
 
305
        // Values specific to the module.
306
        $modulesetvalues = $modulename.'_create_set_values';
307
        $this->$modulesetvalues($moduleinfo);
308
 
309
        // Create the module.
310
        $result = create_module($moduleinfo);
311
 
312
        // Retrieve the module info.
313
        $dbmodinstance = $DB->get_record($moduleinfo->modulename, array('id' => $result->instance));
314
        $dbcm = get_coursemodule_from_instance($moduleinfo->modulename, $result->instance);
315
        // We passed the course section number to create_courses but $dbcm contain the section id.
316
        // We need to retrieve the db course section number.
317
        $section = $DB->get_record('course_sections', array('course' => $dbcm->course, 'id' => $dbcm->section));
318
        // Retrieve the grade item.
319
        $gradeitem = $DB->get_record('grade_items', array('courseid' => $moduleinfo->course,
320
            'iteminstance' => $dbmodinstance->id, 'itemmodule' => $moduleinfo->modulename));
321
 
322
        // Compare the values common to all module instances.
323
        $this->assertEquals($moduleinfo->modulename, $dbcm->modname);
324
        $this->assertEquals($moduleinfo->section, $section->section);
325
        $this->assertEquals($moduleinfo->course, $dbcm->course);
326
        $this->assertEquals($moduleinfo->groupingid, $dbcm->groupingid);
327
        $this->assertEquals($moduleinfo->visible, $dbcm->visible);
328
        $this->assertEquals($moduleinfo->completion, $dbcm->completion);
329
        $this->assertEquals($moduleinfo->completionview, $dbcm->completionview);
330
        $this->assertEquals($moduleinfo->completiongradeitemnumber, $dbcm->completiongradeitemnumber);
331
        $this->assertEquals($moduleinfo->completionpassgrade, $dbcm->completionpassgrade);
332
        $this->assertEquals($moduleinfo->completionexpected, $dbcm->completionexpected);
333
        $this->assertEquals($moduleinfo->availability, $dbcm->availability);
334
        $this->assertEquals($moduleinfo->showdescription, $dbcm->showdescription);
335
        $this->assertEquals($moduleinfo->groupmode, $dbcm->groupmode);
336
        $this->assertEquals($moduleinfo->cmidnumber, $dbcm->idnumber);
337
        $this->assertEquals($moduleinfo->gradecat, $gradeitem->categoryid);
338
 
339
        // Optional grade testing.
340
        if (plugin_supports('mod', $modulename, FEATURE_GRADE_HAS_GRADE, false) && !plugin_supports('mod', $modulename, FEATURE_RATE, false)) {
341
            $this->assertEquals($moduleinfo->grade, $dbmodinstance->grade);
342
        }
343
 
344
        // Some optional (but quite common) to some module.
345
        $this->assertEquals($moduleinfo->name, $dbmodinstance->name);
346
        $this->assertEquals($moduleinfo->intro, $dbmodinstance->intro);
347
        $this->assertEquals($moduleinfo->introformat, $dbmodinstance->introformat);
348
 
349
        // Test specific to the module.
350
        $modulerunasserts = $modulename.'_create_run_asserts';
351
        $this->$modulerunasserts($moduleinfo, $dbmodinstance);
352
        return $moduleinfo;
353
    }
354
 
355
    /**
356
     * Create module associated blog and tags.
357
     *
358
     * @param object $course Course.
359
     * @param object $modulecontext The context of the module.
360
     */
361
    private function create_module_asscociated_blog($course, $modulecontext) {
362
        global $DB, $CFG;
363
 
364
        // Create default group.
365
        $group = new stdClass();
366
        $group->courseid = $course->id;
367
        $group->name = 'Group';
368
        $group->id = $DB->insert_record('groups', $group);
369
 
370
        // Create default user.
371
        $user = $this->getDataGenerator()->create_user(array(
372
            'username' => 'testuser',
373
            'firstname' => 'Firsname',
374
            'lastname' => 'Lastname'
375
        ));
376
 
377
        // Create default post.
378
        $post = new stdClass();
379
        $post->userid = $user->id;
380
        $post->groupid = $group->id;
381
        $post->content = 'test post content text';
382
        $post->module = 'blog';
383
        $post->id = $DB->insert_record('post', $post);
384
 
385
        // Create default tag.
386
        $tag = $this->getDataGenerator()->create_tag(array('userid' => $user->id,
387
            'rawname' => 'Testtagname', 'isstandard' => 1));
388
        // Apply the tag to the blog.
389
        $DB->insert_record('tag_instance', array('tagid' => $tag->id, 'itemtype' => 'user',
390
            'component' => 'core', 'itemid' => $post->id, 'ordering' => 0));
391
 
392
        require_once($CFG->dirroot . '/blog/locallib.php');
393
        $blog = new blog_entry($post->id);
394
        $blog->add_association($modulecontext->id);
395
 
396
        return $blog;
397
    }
398
 
399
    /**
400
     * Test create_module() for multiple modules defined in the $modules array (first declaration of the function).
401
     */
402
    public function test_create_module() {
403
        // Add the module name you want to test here.
404
        // Create the match MODULENAME_create_set_values() and MODULENAME_create_run_asserts().
405
        $modules = array('forum', 'assign');
406
        // Run all tests.
407
        foreach ($modules as $modulename) {
408
            $this->create_specific_module_test($modulename);
409
        }
410
    }
411
 
412
    /**
413
     * Test update_module() for multiple modules defined in the $modules array (first declaration of the function).
414
     */
415
    public function test_update_module() {
416
        // Add the module name you want to test here.
417
        // Create the match MODULENAME_update_set_values() and MODULENAME_update_run_asserts().
418
        $modules = array('forum');
419
        // Run all tests.
420
        foreach ($modules as $modulename) {
421
            $this->update_specific_module_test($modulename);
422
        }
423
    }
424
 
425
    /**
426
     * Set forum specific test values for calling update_module().
427
     *
428
     * @param object $moduleinfo - the moduleinfo to add some specific values - passed in reference.
429
     */
430
    private function forum_update_set_values(&$moduleinfo) {
431
        // Completion specific to forum - optional.
432
        $moduleinfo->completionposts = 3;
433
        $moduleinfo->completiondiscussions = 1;
434
        $moduleinfo->completionreplies = 2;
435
 
436
        // Specific values to the Forum module.
437
        $moduleinfo->forcesubscribe = FORUM_INITIALSUBSCRIBE;
438
        $moduleinfo->type = 'single';
439
        $moduleinfo->trackingtype = FORUM_TRACKING_FORCED;
440
        $moduleinfo->maxbytes = 10240;
441
        $moduleinfo->maxattachments = 2;
442
 
443
        // Post threshold for blocking - specific to forum.
444
        $moduleinfo->blockperiod = 60*60*24;
445
        $moduleinfo->blockafter = 10;
446
        $moduleinfo->warnafter = 5;
447
 
448
        // Grading of whole forum settings.
449
        $moduleinfo->grade_forum = 0;
450
    }
451
 
452
    /**
453
     * Execute test asserts on the saved DB data by update_module($forum).
454
     *
455
     * @param object $moduleinfo - the specific forum values that were used to update a forum.
456
     * @param object $dbmodinstance - the DB values of the updated forum.
457
     */
458
    private function forum_update_run_asserts($moduleinfo, $dbmodinstance) {
459
        // Compare values specific to forums.
460
        $this->assertEquals($moduleinfo->forcesubscribe, $dbmodinstance->forcesubscribe);
461
        $this->assertEquals($moduleinfo->type, $dbmodinstance->type);
462
        $this->assertEquals($moduleinfo->assessed, $dbmodinstance->assessed);
463
        $this->assertEquals($moduleinfo->completionposts, $dbmodinstance->completionposts);
464
        $this->assertEquals($moduleinfo->completiondiscussions, $dbmodinstance->completiondiscussions);
465
        $this->assertEquals($moduleinfo->completionreplies, $dbmodinstance->completionreplies);
466
        $this->assertEquals($moduleinfo->scale, $dbmodinstance->scale);
467
        $this->assertEquals($moduleinfo->assesstimestart, $dbmodinstance->assesstimestart);
468
        $this->assertEquals($moduleinfo->assesstimefinish, $dbmodinstance->assesstimefinish);
469
        $this->assertEquals($moduleinfo->rsstype, $dbmodinstance->rsstype);
470
        $this->assertEquals($moduleinfo->rssarticles, $dbmodinstance->rssarticles);
471
        $this->assertEquals($moduleinfo->trackingtype, $dbmodinstance->trackingtype);
472
        $this->assertEquals($moduleinfo->maxbytes, $dbmodinstance->maxbytes);
473
        $this->assertEquals($moduleinfo->maxattachments, $dbmodinstance->maxattachments);
474
        $this->assertEquals($moduleinfo->blockperiod, $dbmodinstance->blockperiod);
475
        $this->assertEquals($moduleinfo->blockafter, $dbmodinstance->blockafter);
476
        $this->assertEquals($moduleinfo->warnafter, $dbmodinstance->warnafter);
477
    }
478
 
479
 
480
 
481
    /**
482
     * Test a specific type of module.
483
     *
484
     * @param string $modulename - the module name to test
485
     */
486
    private function update_specific_module_test($modulename) {
487
        global $DB, $CFG;
488
 
489
        $this->resetAfterTest(true);
490
 
491
        $this->setAdminUser();
492
 
493
        // Warnings: you'll need to change this line if ever you come to test a module not following Moodle standard.
494
        require_once($CFG->dirroot.'/mod/'. $modulename .'/lib.php');
495
 
496
        // Enable avaibility.
497
        // If not enabled all conditional fields will be ignored.
498
        set_config('enableavailability', 1);
499
 
500
        // Enable course completion.
501
        // If not enabled all completion settings will be ignored.
502
        set_config('enablecompletion', COMPLETION_ENABLED);
503
 
504
        // Enable forum RSS feeds.
505
        set_config('enablerssfeeds', 1);
506
        set_config('forum_enablerssfeeds', 1);
507
 
508
        $course = $this->getDataGenerator()->create_course(array('numsections'=>1, 'enablecompletion' => COMPLETION_ENABLED),
509
           array('createsections'=>true));
510
 
511
        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
512
 
513
        // Create assign module instance for testing gradeitem.
514
        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
515
        $params['course'] = $course->id;
516
        $instance = $generator->create_instance($params);
517
        $assigncm = get_coursemodule_from_instance('assign', $instance->id);
518
 
519
        // Create the test forum to update.
520
        $initvalues = new stdClass();
521
        $initvalues->introformat = FORMAT_HTML;
522
        $initvalues->course = $course->id;
523
        $forum = self::getDataGenerator()->create_module('forum', $initvalues);
524
 
525
        // Retrieve course module.
526
        $cm = get_coursemodule_from_instance('forum', $forum->id);
527
 
528
        // Module test values.
529
        $moduleinfo = new stdClass();
530
 
531
        // Always mandatory generic values to any module.
532
        $moduleinfo->coursemodule = $cm->id;
533
        $moduleinfo->modulename = $modulename;
534
        $moduleinfo->course = $course->id;
535
        $moduleinfo->groupingid = $grouping->id;
536
        $moduleinfo->visible = true;
537
        $moduleinfo->visibleoncoursepage = true;
538
 
539
        // Sometimes optional generic values for some modules.
540
        $moduleinfo->name = 'My test module';
541
        $moduleinfo->showdescription = 1; // standard boolean
542
        require_once($CFG->libdir . '/gradelib.php');
543
        $gradecats = grade_get_categories_menu($moduleinfo->course, false);
544
        $gradecatid = current(array_keys($gradecats)); // Retrieve the first key of $gradecats
545
        $moduleinfo->gradecat = $gradecatid;
546
        $moduleinfo->groupmode = VISIBLEGROUPS;
547
        $moduleinfo->cmidnumber = 'idnumber_XXX';
548
 
549
        // Completion common to all module.
550
        $moduleinfo->completion = COMPLETION_TRACKING_AUTOMATIC;
551
        $moduleinfo->completionview = COMPLETION_VIEW_REQUIRED;
552
        $moduleinfo->completiongradeitemnumber = 1;
553
        $moduleinfo->completionpassgrade = 0;
554
        $moduleinfo->completionexpected = time() + (7 * 24 * 3600);
555
        $moduleinfo->completionunlocked = 1;
556
 
557
        // Conditional activity.
558
        $coursegradeitem = grade_item::fetch_course_item($moduleinfo->course); //the activity will become available only when the user reach some grade into the course itself.
559
        $moduleinfo->availability = json_encode(\core_availability\tree::get_root_json(
560
                array(\availability_date\condition::get_json('>=', time()),
561
                \availability_date\condition::get_json('<', time() + (7 * 24 * 3600)),
562
                \availability_grade\condition::get_json($coursegradeitem->id, 10, 80),
563
                \availability_profile\condition::get_json(false, 'email', 'contains', '@'),
564
                \availability_completion\condition::get_json($assigncm->id, COMPLETION_COMPLETE)), '&'));
565
 
566
        // Grading and Advanced grading.
567
        require_once($CFG->dirroot . '/rating/lib.php');
568
        $moduleinfo->assessed = RATING_AGGREGATE_AVERAGE;
569
        $moduleinfo->scale = 10; // Note: it could be minus (for specific course scale). It is a signed number.
570
        $moduleinfo->assesstimestart = time();
571
        $moduleinfo->assesstimefinish = time() + (7 * 24 * 3600);
572
 
573
        // RSS.
574
        $moduleinfo->rsstype = 2;
575
        $moduleinfo->rssarticles = 10;
576
 
577
        // Optional intro editor (depends of module).
578
        $draftid_editor = 0;
579
        file_prepare_draft_area($draftid_editor, null, null, null, null);
580
        $moduleinfo->introeditor = array('text' => 'This is a module', 'format' => FORMAT_HTML, 'itemid' => $draftid_editor);
581
 
582
        // Following is the advanced grading method area called 'submissions' for the 'assign' module.
583
        if (plugin_supports('mod', $modulename, FEATURE_GRADE_HAS_GRADE, false) && !plugin_supports('mod', $modulename, FEATURE_RATE, false)) {
584
            $moduleinfo->grade = 100;
585
        }
586
        // Plagiarism form values.
587
        // No plagiarism plugin installed by default. Use this space to make your own test.
588
 
589
        // Values specific to the module.
590
        $modulesetvalues = $modulename.'_update_set_values';
591
        $this->$modulesetvalues($moduleinfo);
592
 
593
        // Create the module.
594
        $result = update_module($moduleinfo);
595
 
596
        // Retrieve the module info.
597
        $dbmodinstance = $DB->get_record($moduleinfo->modulename, array('id' => $result->instance));
598
        $dbcm = get_coursemodule_from_instance($moduleinfo->modulename, $result->instance);
599
        // Retrieve the grade item.
600
        $gradeitem = $DB->get_record('grade_items', array('courseid' => $moduleinfo->course,
601
            'iteminstance' => $dbmodinstance->id, 'itemmodule' => $moduleinfo->modulename));
602
 
603
        // Compare the values common to all module instances.
604
        $this->assertEquals($moduleinfo->modulename, $dbcm->modname);
605
        $this->assertEquals($moduleinfo->course, $dbcm->course);
606
        $this->assertEquals($moduleinfo->groupingid, $dbcm->groupingid);
607
        $this->assertEquals($moduleinfo->visible, $dbcm->visible);
608
        $this->assertEquals($moduleinfo->completion, $dbcm->completion);
609
        $this->assertEquals($moduleinfo->completionview, $dbcm->completionview);
610
        $this->assertEquals($moduleinfo->completiongradeitemnumber, $dbcm->completiongradeitemnumber);
611
        $this->assertEquals($moduleinfo->completionpassgrade, $dbcm->completionpassgrade);
612
        $this->assertEquals($moduleinfo->completionexpected, $dbcm->completionexpected);
613
        $this->assertEquals($moduleinfo->availability, $dbcm->availability);
614
        $this->assertEquals($moduleinfo->showdescription, $dbcm->showdescription);
615
        $this->assertEquals($moduleinfo->groupmode, $dbcm->groupmode);
616
        $this->assertEquals($moduleinfo->cmidnumber, $dbcm->idnumber);
617
        $this->assertEquals($moduleinfo->gradecat, $gradeitem->categoryid);
618
 
619
        // Optional grade testing.
620
        if (plugin_supports('mod', $modulename, FEATURE_GRADE_HAS_GRADE, false) && !plugin_supports('mod', $modulename, FEATURE_RATE, false)) {
621
            $this->assertEquals($moduleinfo->grade, $dbmodinstance->grade);
622
        }
623
 
624
        // Some optional (but quite common) to some module.
625
        $this->assertEquals($moduleinfo->name, $dbmodinstance->name);
626
        $this->assertEquals($moduleinfo->intro, $dbmodinstance->intro);
627
        $this->assertEquals($moduleinfo->introformat, $dbmodinstance->introformat);
628
 
629
        // Test specific to the module.
630
        $modulerunasserts = $modulename.'_update_run_asserts';
631
        $this->$modulerunasserts($moduleinfo, $dbmodinstance);
632
        return $moduleinfo;
633
   }
634
 
635
    /**
636
     * Data provider for course_delete module
637
     *
638
     * @return array An array of arrays contain test data
639
     */
640
    public function provider_course_delete_module() {
641
        $data = array();
642
 
643
        $data['assign'] = array('assign', array('duedate' => time()));
644
        $data['quiz'] = array('quiz', array('duedate' => time()));
645
 
646
        return $data;
647
    }
648
 
649
    /**
650
     * Test the create_course function
651
     */
652
    public function test_create_course() {
653
        global $DB;
654
        $this->resetAfterTest(true);
655
        $defaultcategory = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
656
 
657
        $course = new stdClass();
658
        $course->fullname = 'Apu loves Unit Təsts';
659
        $course->shortname = 'Spread the lŭve';
660
        $course->idnumber = '123';
661
        $course->summary = 'Awesome!';
662
        $course->summaryformat = FORMAT_PLAIN;
663
        $course->format = 'topics';
664
        $course->newsitems = 0;
665
        $course->category = $defaultcategory;
666
        $original = (array) $course;
667
 
668
        $created = create_course($course);
669
        $context = context_course::instance($created->id);
670
 
671
        // Compare original and created.
672
        $this->assertEquals($original, array_intersect_key((array) $created, $original));
673
 
674
        // Ensure default section is created.
675
        $sectioncreated = $DB->record_exists('course_sections', array('course' => $created->id, 'section' => 0));
676
        $this->assertTrue($sectioncreated);
677
 
678
        // Ensure that the shortname isn't duplicated.
679
        try {
680
            $created = create_course($course);
681
            $this->fail('Exception expected');
682
        } catch (moodle_exception $e) {
683
            $this->assertSame(get_string('shortnametaken', 'error', $course->shortname), $e->getMessage());
684
        }
685
 
686
        // Ensure that the idnumber isn't duplicated.
687
        $course->shortname .= '1';
688
        try {
689
            $created = create_course($course);
690
            $this->fail('Exception expected');
691
        } catch (moodle_exception $e) {
692
            $this->assertSame(get_string('courseidnumbertaken', 'error', $course->idnumber), $e->getMessage());
693
        }
694
    }
695
 
696
    public function test_create_course_with_generator() {
697
        global $DB;
698
        $this->resetAfterTest(true);
699
        $course = $this->getDataGenerator()->create_course();
700
 
701
        // Ensure default section is created.
702
        $sectioncreated = $DB->record_exists('course_sections', array('course' => $course->id, 'section' => 0));
703
        $this->assertTrue($sectioncreated);
704
    }
705
 
706
    public function test_create_course_sections() {
707
        global $DB;
708
        $this->resetAfterTest(true);
709
 
710
        $numsections = 5;
711
        $course = $this->getDataGenerator()->create_course(
712
                array('shortname' => 'GrowingCourse',
713
                    'fullname' => 'Growing Course',
714
                    'numsections' => $numsections),
715
                array('createsections' => true));
716
 
717
        // Ensure all 6 (0-5) sections were created and course content cache works properly
718
        $sectionscreated = array_keys(get_fast_modinfo($course)->get_section_info_all());
719
        $this->assertEquals(range(0, $numsections), $sectionscreated);
720
 
721
        // this will do nothing, section already exists
722
        $this->assertFalse(course_create_sections_if_missing($course, $numsections));
723
 
724
        // this will create new section
725
        $this->assertTrue(course_create_sections_if_missing($course, $numsections + 1));
726
 
727
        // Ensure all 7 (0-6) sections were created and modinfo/sectioninfo cache works properly
728
        $sectionscreated = array_keys(get_fast_modinfo($course)->get_section_info_all());
729
        $this->assertEquals(range(0, $numsections + 1), $sectionscreated);
730
    }
731
 
732
    public function test_update_course() {
733
        global $DB;
734
 
735
        $this->resetAfterTest();
736
 
737
        $defaultcategory = $DB->get_field_select('course_categories', 'MIN(id)', 'parent = 0');
738
 
739
        $course = new stdClass();
740
        $course->fullname = 'Apu loves Unit Təsts';
741
        $course->shortname = 'test1';
742
        $course->idnumber = '1';
743
        $course->summary = 'Awesome!';
744
        $course->summaryformat = FORMAT_PLAIN;
745
        $course->format = 'topics';
746
        $course->newsitems = 0;
747
        $course->numsections = 5;
748
        $course->category = $defaultcategory;
749
 
750
        $created = create_course($course);
751
        // Ensure the checks only work on idnumber/shortname that are not already ours.
752
        update_course($created);
753
 
754
        $course->shortname = 'test2';
755
        $course->idnumber = '2';
756
 
757
        $created2 = create_course($course);
758
 
759
        // Test duplicate idnumber.
760
        $created2->idnumber = '1';
761
        try {
762
            update_course($created2);
763
            $this->fail('Expected exception when trying to update a course with duplicate idnumber');
764
        } catch (moodle_exception $e) {
765
            $this->assertEquals(get_string('courseidnumbertaken', 'error', $created2->idnumber), $e->getMessage());
766
        }
767
 
768
        // Test duplicate shortname.
769
        $created2->idnumber = '2';
770
        $created2->shortname = 'test1';
771
        try {
772
            update_course($created2);
773
            $this->fail('Expected exception when trying to update a course with a duplicate shortname');
774
        } catch (moodle_exception $e) {
775
            $this->assertEquals(get_string('shortnametaken', 'error', $created2->shortname), $e->getMessage());
776
        }
777
    }
778
 
779
    public function test_update_course_section_time_modified() {
780
        global $DB;
781
 
782
        $this->resetAfterTest();
783
 
784
        // Create the course with sections.
785
        $course = $this->getDataGenerator()->create_course(
786
            ['numsections' => 10],
787
            ['createsections' => true]
788
        );
789
        $sections = $DB->get_records('course_sections', ['course' => $course->id]);
790
 
791
        // Get the last section's time modified value.
792
        $section = array_pop($sections);
793
        $oldtimemodified = $section->timemodified;
794
 
795
        // Ensuring that the section update occurs at a different timestamp.
796
        $this->waitForSecond();
797
 
798
        // The timemodified should only be updated if the section is actually updated.
799
        course_update_section($course, $section, []);
800
        $sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
801
        $this->assertEquals($oldtimemodified, $sectionrecord->timemodified);
802
 
803
        // Now update something to prove timemodified changes.
804
        course_update_section($course, $section, ['name' => 'New name']);
805
        $section = $DB->get_record('course_sections', ['id' => $section->id]);
806
        $newtimemodified = $section->timemodified;
807
        $this->assertGreaterThan($oldtimemodified, $newtimemodified);
808
    }
809
 
810
    /**
811
     * Relative dates mode settings provider for course creation.
812
     */
813
    public function create_course_relative_dates_provider() {
814
        return [
815
            [0, 0, 0],
816
            [0, 1, 0],
817
            [1, 0, 0],
818
            [1, 1, 1],
819
        ];
820
    }
821
 
822
    /**
823
     * Test create_course by attempting to change the relative dates mode.
824
     *
825
     * @dataProvider create_course_relative_dates_provider
826
     * @param int $setting The value for the 'enablecourserelativedates' admin setting.
827
     * @param int $mode The value for the course's 'relativedatesmode' field.
828
     * @param int $expectedvalue The expected value of the 'relativedatesmode' field after course creation.
829
     */
830
    public function test_relative_dates_mode_for_course_creation($setting, $mode, $expectedvalue) {
831
        global $DB;
832
 
833
        $this->resetAfterTest();
834
 
835
        set_config('enablecourserelativedates', $setting);
836
 
837
        // Generate a course with relative dates mode set to $mode.
838
        $course = $this->getDataGenerator()->create_course(['relativedatesmode' => $mode]);
839
 
840
        // Verify that the relative dates match what's expected.
841
        $relativedatesmode = $DB->get_field('course', 'relativedatesmode', ['id' => $course->id]);
842
        $this->assertEquals($expectedvalue, $relativedatesmode);
843
    }
844
 
845
    /**
846
     * Test update_course by attempting to change the relative dates mode.
847
     */
848
    public function test_relative_dates_mode_for_course_update() {
849
        global $DB;
850
 
851
        $this->resetAfterTest();
852
 
853
        set_config('enablecourserelativedates', 1);
854
 
855
        // Generate a course with relative dates mode set to 1.
856
        $course = $this->getDataGenerator()->create_course(['relativedatesmode' => 1]);
857
 
858
        // Attempt to update the course with a changed relativedatesmode.
859
        $course->relativedatesmode = 0;
860
        update_course($course);
861
 
862
        // Verify that the relative dates mode has not changed.
863
        $relativedatesmode = $DB->get_field('course', 'relativedatesmode', ['id' => $course->id]);
864
        $this->assertEquals(1, $relativedatesmode);
865
    }
866
 
867
    public function test_course_add_cm_to_section() {
868
        global $DB;
869
        $this->resetAfterTest(true);
870
 
871
        // Create course with 1 section.
872
        $course = $this->getDataGenerator()->create_course(
873
                array('shortname' => 'GrowingCourse',
874
                    'fullname' => 'Growing Course',
875
                    'numsections' => 1),
876
                array('createsections' => true));
877
 
878
        // Trash modinfo.
879
        rebuild_course_cache($course->id, true);
880
 
881
        // Create some cms for testing.
882
        $cmids = array();
883
        for ($i=0; $i<4; $i++) {
884
            $cmids[$i] = $DB->insert_record('course_modules', array('course' => $course->id));
885
        }
886
 
887
        // Add it to section that exists.
888
        course_add_cm_to_section($course, $cmids[0], 1);
889
 
890
        // Check it got added to sequence.
891
        $sequence = $DB->get_field('course_sections', 'sequence', array('course' => $course->id, 'section' => 1));
892
        $this->assertEquals($cmids[0], $sequence);
893
 
894
        // Add a second, this time using courseid variant of parameters.
895
        $coursecacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
896
        course_add_cm_to_section($course->id, $cmids[1], 1);
897
        $sequence = $DB->get_field('course_sections', 'sequence', array('course' => $course->id, 'section' => 1));
898
        $this->assertEquals($cmids[0] . ',' . $cmids[1], $sequence);
899
 
900
        // Check that modinfo cache was reset but not rebuilt (important for performance if calling repeatedly).
901
        $newcacherev = $DB->get_field('course', 'cacherev', ['id' => $course->id]);
902
        $this->assertGreaterThan($coursecacherev, $newcacherev);
903
        $this->assertEmpty(cache::make('core', 'coursemodinfo')->get_versioned($course->id, $newcacherev));
904
 
905
        // Add one to section that doesn't exist (this might rebuild modinfo).
906
        course_add_cm_to_section($course, $cmids[2], 2);
907
        $this->assertEquals(3, $DB->count_records('course_sections', array('course' => $course->id)));
908
        $sequence = $DB->get_field('course_sections', 'sequence', array('course' => $course->id, 'section' => 2));
909
        $this->assertEquals($cmids[2], $sequence);
910
 
911
        // Add using the 'before' option.
912
        course_add_cm_to_section($course, $cmids[3], 2, $cmids[2]);
913
        $this->assertEquals(3, $DB->count_records('course_sections', array('course' => $course->id)));
914
        $sequence = $DB->get_field('course_sections', 'sequence', array('course' => $course->id, 'section' => 2));
915
        $this->assertEquals($cmids[3] . ',' . $cmids[2], $sequence);
916
    }
917
 
918
    public function test_reorder_sections() {
919
        global $DB;
920
        $this->resetAfterTest(true);
921
 
922
        $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections'=>true));
923
        $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true));
924
        $oldsections = array();
925
        $sections = array();
926
        foreach ($DB->get_records('course_sections', array('course'=>$course->id), 'id') as $section) {
927
            $oldsections[$section->section] = $section->id;
928
            $sections[$section->id] = $section->section;
929
        }
930
        ksort($oldsections);
931
 
932
        $neworder = reorder_sections($sections, 2, 4);
933
        $neworder = array_keys($neworder);
934
        $this->assertEquals($oldsections[0], $neworder[0]);
935
        $this->assertEquals($oldsections[1], $neworder[1]);
936
        $this->assertEquals($oldsections[2], $neworder[4]);
937
        $this->assertEquals($oldsections[3], $neworder[2]);
938
        $this->assertEquals($oldsections[4], $neworder[3]);
939
        $this->assertEquals($oldsections[5], $neworder[5]);
940
        $this->assertEquals($oldsections[6], $neworder[6]);
941
 
942
        $neworder = reorder_sections($sections, 4, 2);
943
        $neworder = array_keys($neworder);
944
        $this->assertEquals($oldsections[0], $neworder[0]);
945
        $this->assertEquals($oldsections[1], $neworder[1]);
946
        $this->assertEquals($oldsections[2], $neworder[3]);
947
        $this->assertEquals($oldsections[3], $neworder[4]);
948
        $this->assertEquals($oldsections[4], $neworder[2]);
949
        $this->assertEquals($oldsections[5], $neworder[5]);
950
        $this->assertEquals($oldsections[6], $neworder[6]);
951
 
952
        $neworder = reorder_sections(1, 2, 4);
953
        $this->assertFalse($neworder);
954
    }
955
 
956
    public function test_move_section_down() {
957
        global $DB;
958
        $this->resetAfterTest(true);
959
 
960
        $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections'=>true));
961
        $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true));
962
        $oldsections = array();
963
        foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) {
964
            $oldsections[$section->section] = $section->id;
965
        }
966
        ksort($oldsections);
967
 
968
        // Test move section down..
969
        move_section_to($course, 2, 4);
970
        $sections = array();
971
        foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) {
972
            $sections[$section->section] = $section->id;
973
        }
974
        ksort($sections);
975
 
976
        $this->assertEquals($oldsections[0], $sections[0]);
977
        $this->assertEquals($oldsections[1], $sections[1]);
978
        $this->assertEquals($oldsections[2], $sections[4]);
979
        $this->assertEquals($oldsections[3], $sections[2]);
980
        $this->assertEquals($oldsections[4], $sections[3]);
981
        $this->assertEquals($oldsections[5], $sections[5]);
982
        $this->assertEquals($oldsections[6], $sections[6]);
983
    }
984
 
985
    public function test_move_section_up() {
986
        global $DB;
987
        $this->resetAfterTest(true);
988
 
989
        $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections'=>true));
990
        $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true));
991
        $oldsections = array();
992
        foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) {
993
            $oldsections[$section->section] = $section->id;
994
        }
995
        ksort($oldsections);
996
 
997
        // Test move section up..
998
        move_section_to($course, 6, 4);
999
        $sections = array();
1000
        foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) {
1001
            $sections[$section->section] = $section->id;
1002
        }
1003
        ksort($sections);
1004
 
1005
        $this->assertEquals($oldsections[0], $sections[0]);
1006
        $this->assertEquals($oldsections[1], $sections[1]);
1007
        $this->assertEquals($oldsections[2], $sections[2]);
1008
        $this->assertEquals($oldsections[3], $sections[3]);
1009
        $this->assertEquals($oldsections[4], $sections[5]);
1010
        $this->assertEquals($oldsections[5], $sections[6]);
1011
        $this->assertEquals($oldsections[6], $sections[4]);
1012
    }
1013
 
1014
    public function test_move_section_marker() {
1015
        global $DB;
1016
        $this->resetAfterTest(true);
1017
 
1018
        $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections'=>true));
1019
        $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true));
1020
 
1021
        // Set course marker to the section we are going to move..
1022
        course_set_marker($course->id, 2);
1023
        // Verify that the course marker is set correctly.
1024
        $course = $DB->get_record('course', array('id' => $course->id));
1025
        $this->assertEquals(2, $course->marker);
1026
 
1027
        // Test move the marked section down..
1028
        move_section_to($course, 2, 4);
1029
 
1030
        // Verify that the course marker has been moved along with the section..
1031
        $course = $DB->get_record('course', array('id' => $course->id));
1032
        $this->assertEquals(4, $course->marker);
1033
 
1034
        // Test move the marked section up..
1035
        move_section_to($course, 4, 3);
1036
 
1037
        // Verify that the course marker has been moved along with the section..
1038
        $course = $DB->get_record('course', array('id' => $course->id));
1039
        $this->assertEquals(3, $course->marker);
1040
 
1041
        // Test moving a non-marked section above the marked section..
1042
        move_section_to($course, 4, 2);
1043
 
1044
        // Verify that the course marker has been moved down to accomodate..
1045
        $course = $DB->get_record('course', array('id' => $course->id));
1046
        $this->assertEquals(4, $course->marker);
1047
 
1048
        // Test moving a non-marked section below the marked section..
1049
        move_section_to($course, 3, 6);
1050
 
1051
        // Verify that the course marker has been up to accomodate..
1052
        $course = $DB->get_record('course', array('id' => $course->id));
1053
        $this->assertEquals(3, $course->marker);
1054
    }
1055
 
1056
    /**
1057
     * Test move_section_to method with caching
1058
     *
1059
     * @covers ::move_section_to
1060
     * @return void
1061
     */
1062
    public function test_move_section_with_section_cache(): void {
1063
        $this->resetAfterTest();
1064
        $this->setAdminUser();
1065
        $cache = cache::make('core', 'coursemodinfo');
1066
 
1067
        // Generate the course and pre-requisite module.
1068
        $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
1069
        // Reset course cache.
1070
        rebuild_course_cache($course->id, true);
1071
 
1072
        // Build course cache.
1073
        $modinfo = get_fast_modinfo($course->id);
1074
        // Get the course modinfo cache.
1075
        $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1076
        // Get the section cache.
1077
        $sectioncaches = $coursemodinfo->sectioncache;
1078
 
1079
        $numberedsections = $modinfo->get_section_info_all();
1080
 
1081
        // Make sure that we will have 4 section caches here.
1082
        $this->assertCount(4, $sectioncaches);
1083
        $this->assertArrayHasKey($numberedsections[0]->id, $sectioncaches);
1084
        $this->assertArrayHasKey($numberedsections[1]->id, $sectioncaches);
1085
        $this->assertArrayHasKey($numberedsections[2]->id, $sectioncaches);
1086
        $this->assertArrayHasKey($numberedsections[3]->id, $sectioncaches);
1087
 
1088
        // Move section.
1089
        move_section_to($course, 2, 3);
1090
        // Get the course modinfo cache.
1091
        $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1092
        // Get the section cache.
1093
        $sectioncaches = $coursemodinfo->sectioncache;
1094
 
1095
        // Make sure that we will have 2 section caches left.
1096
        $this->assertCount(2, $sectioncaches);
1097
        $this->assertArrayHasKey($numberedsections[0]->id, $sectioncaches);
1098
        $this->assertArrayHasKey($numberedsections[1]->id, $sectioncaches);
1099
        $this->assertArrayNotHasKey($numberedsections[2]->id, $sectioncaches);
1100
        $this->assertArrayNotHasKey($numberedsections[3]->id, $sectioncaches);
1101
    }
1102
 
1103
    /**
1104
     * Test move_section_to method.
1105
     * Make sure that we only update the moving sections, not all the sections in the current course.
1106
     *
1107
     * @covers ::move_section_to
1108
     * @return void
1109
     */
1110
    public function test_move_section_to(): void {
1111
        global $DB, $CFG;
1112
        $this->resetAfterTest();
1113
        $this->setAdminUser();
1114
 
1115
        // Generate the course and pre-requisite module.
1116
        $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
1117
 
1118
        ob_start();
1119
        $DB->set_debug(true);
1120
        // Move section.
1121
        move_section_to($course, 2, 3);
1122
        $DB->set_debug(false);
1123
        $debuginfo = ob_get_contents();
1124
        ob_end_clean();
1125
        $sectionmovequerycount = substr_count($debuginfo, 'UPDATE ' . $CFG->phpunit_prefix . 'course_sections SET');
1126
        // We are updating the course_section table in steps to avoid breaking database uniqueness constraint.
1127
        // So the queries will be doubled. See: course/lib.php:1423
1128
        // Make sure that we only need 4 queries to update the position of section 2 and section 3.
1129
        $this->assertEquals(4, $sectionmovequerycount);
1130
    }
1131
 
1132
    public function test_course_can_delete_section() {
1133
        global $DB;
1134
        $this->resetAfterTest(true);
1135
 
1136
        $generator = $this->getDataGenerator();
1137
 
1138
        $courseweeks = $generator->create_course(
1139
            array('numsections' => 5, 'format' => 'weeks'),
1140
            array('createsections' => true));
1141
        $assign1 = $generator->create_module('assign', array('course' => $courseweeks, 'section' => 1));
1142
        $assign2 = $generator->create_module('assign', array('course' => $courseweeks, 'section' => 2));
1143
 
1144
        $coursetopics = $generator->create_course(
1145
            array('numsections' => 5, 'format' => 'topics'),
1146
            array('createsections' => true));
1147
 
1148
        $coursesingleactivity = $generator->create_course(
1149
            array('format' => 'singleactivity'),
1150
            array('createsections' => true));
1151
 
1152
        // Enrol student and teacher.
1153
        $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
1154
        $student = $generator->create_user();
1155
        $teacher = $generator->create_user();
1156
 
1157
        $generator->enrol_user($student->id, $courseweeks->id, $roleids['student']);
1158
        $generator->enrol_user($teacher->id, $courseweeks->id, $roleids['editingteacher']);
1159
 
1160
        $generator->enrol_user($student->id, $coursetopics->id, $roleids['student']);
1161
        $generator->enrol_user($teacher->id, $coursetopics->id, $roleids['editingteacher']);
1162
 
1163
        $generator->enrol_user($student->id, $coursesingleactivity->id, $roleids['student']);
1164
        $generator->enrol_user($teacher->id, $coursesingleactivity->id, $roleids['editingteacher']);
1165
 
1166
        // Teacher should be able to delete sections (except for 0) in topics and weeks format.
1167
        $this->setUser($teacher);
1168
 
1169
        // For topics and weeks formats will return false for section 0 and true for any other section.
1170
        $this->assertFalse(course_can_delete_section($courseweeks, 0));
1171
        $this->assertTrue(course_can_delete_section($courseweeks, 1));
1172
 
1173
        $this->assertFalse(course_can_delete_section($coursetopics, 0));
1174
        $this->assertTrue(course_can_delete_section($coursetopics, 1));
1175
 
1176
        // For singleactivity course format no section can be deleted.
1177
        $this->assertFalse(course_can_delete_section($coursesingleactivity, 0));
1178
        $this->assertFalse(course_can_delete_section($coursesingleactivity, 1));
1179
 
1180
        // Now let's revoke a capability from teacher to manage activity in section 1.
1181
        $modulecontext = context_module::instance($assign1->cmid);
1182
        assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleids['editingteacher'],
1183
            $modulecontext);
1184
        $this->assertFalse(course_can_delete_section($courseweeks, 1));
1185
        $this->assertTrue(course_can_delete_section($courseweeks, 2));
1186
 
1187
        // Student does not have permissions to delete sections.
1188
        $this->setUser($student);
1189
        $this->assertFalse(course_can_delete_section($courseweeks, 1));
1190
        $this->assertFalse(course_can_delete_section($coursetopics, 1));
1191
        $this->assertFalse(course_can_delete_section($coursesingleactivity, 1));
1192
    }
1193
 
1194
    public function test_course_delete_section() {
1195
        global $DB;
1196
        $this->resetAfterTest(true);
1197
 
1198
        $generator = $this->getDataGenerator();
1199
 
1200
        $course = $generator->create_course(array('numsections' => 6, 'format' => 'topics'),
1201
            array('createsections' => true));
1202
        $assign0 = $generator->create_module('assign', array('course' => $course, 'section' => 0));
1203
        $assign1 = $generator->create_module('assign', array('course' => $course, 'section' => 1));
1204
        $assign21 = $generator->create_module('assign', array('course' => $course, 'section' => 2));
1205
        $assign22 = $generator->create_module('assign', array('course' => $course, 'section' => 2));
1206
        $assign3 = $generator->create_module('assign', array('course' => $course, 'section' => 3));
1207
        $assign5 = $generator->create_module('assign', array('course' => $course, 'section' => 5));
1208
        $assign6 = $generator->create_module('assign', array('course' => $course, 'section' => 6));
1209
 
1210
        $this->setAdminUser();
1211
 
1212
        // Attempt to delete non-existing section.
1213
        $this->assertFalse(course_delete_section($course, 10, false));
1214
        $this->assertFalse(course_delete_section($course, 9, true));
1215
 
1216
        // Attempt to delete 0-section.
1217
        $this->assertFalse(course_delete_section($course, 0, true));
1218
        $this->assertTrue($DB->record_exists('course_modules', array('id' => $assign0->cmid)));
1219
 
1220
        // Delete last section.
1221
        $this->assertTrue(course_delete_section($course, 6, true));
1222
        $this->assertFalse($DB->record_exists('course_modules', array('id' => $assign6->cmid)));
1223
        $this->assertEquals(5, course_get_format($course)->get_last_section_number());
1224
 
1225
        // Delete empty section.
1226
        $this->assertTrue(course_delete_section($course, 4, false));
1227
        $this->assertEquals(4, course_get_format($course)->get_last_section_number());
1228
 
1229
        // Delete section in the middle (2).
1230
        $this->assertFalse(course_delete_section($course, 2, false));
1231
        $this->assertEquals(4, course_get_format($course)->get_last_section_number());
1232
        $this->assertTrue(course_delete_section($course, 2, true));
1233
        $this->assertFalse($DB->record_exists('course_modules', array('id' => $assign21->cmid)));
1234
        $this->assertFalse($DB->record_exists('course_modules', array('id' => $assign22->cmid)));
1235
        $this->assertEquals(3, course_get_format($course)->get_last_section_number());
1236
        $this->assertEquals(array(0 => array($assign0->cmid),
1237
            1 => array($assign1->cmid),
1238
            2 => array($assign3->cmid),
1239
            3 => array($assign5->cmid)), get_fast_modinfo($course)->sections);
1240
 
1241
        // Remove marked section.
1242
        course_set_marker($course->id, 1);
1243
        $this->assertTrue(course_get_format($course)->is_section_current(1));
1244
        $this->assertTrue(course_delete_section($course, 1, true));
1245
        $this->assertFalse(course_get_format($course)->is_section_current(1));
1246
    }
1247
 
1248
    public function test_get_course_display_name_for_list() {
1249
        global $CFG;
1250
        $this->resetAfterTest(true);
1251
 
1252
        $course = $this->getDataGenerator()->create_course(array('shortname' => 'FROG101', 'fullname' => 'Introduction to pond life'));
1253
 
1254
        $CFG->courselistshortnames = 0;
1255
        $this->assertEquals('Introduction to pond life', get_course_display_name_for_list($course));
1256
 
1257
        $CFG->courselistshortnames = 1;
1258
        $this->assertEquals('FROG101 Introduction to pond life', get_course_display_name_for_list($course));
1259
    }
1260
 
1261
    public function test_move_module_in_course() {
1262
        global $DB;
1263
 
1264
        $this->resetAfterTest(true);
1265
        // Setup fixture
1266
        $course = $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections' => true));
1267
        $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
1268
 
1269
        $cms = get_fast_modinfo($course)->get_cms();
1270
        $cm = reset($cms);
1271
 
1272
        $newsection = get_fast_modinfo($course)->get_section_info(3);
1273
        $oldsectionid = $cm->section;
1274
 
1275
        // Perform the move
1276
        moveto_module($cm, $newsection);
1277
 
1278
        $cms = get_fast_modinfo($course)->get_cms();
1279
        $cm = reset($cms);
1280
 
1281
        // Check that the cached modinfo contains the correct section info
1282
        $modinfo = get_fast_modinfo($course);
1283
        $this->assertTrue(empty($modinfo->sections[0]));
1284
        $this->assertFalse(empty($modinfo->sections[3]));
1285
 
1286
        // Check that the old section's sequence no longer contains this ID
1287
        $oldsection = $DB->get_record('course_sections', array('id' => $oldsectionid));
1288
        $oldsequences = explode(',', $newsection->sequence);
1289
        $this->assertFalse(in_array($cm->id, $oldsequences));
1290
 
1291
        // Check that the new section's sequence now contains this ID
1292
        $newsection = $DB->get_record('course_sections', array('id' => $newsection->id));
1293
        $newsequences = explode(',', $newsection->sequence);
1294
        $this->assertTrue(in_array($cm->id, $newsequences));
1295
 
1296
        // Check that the section number has been changed in the cm
1297
        $this->assertEquals($newsection->id, $cm->section);
1298
 
1299
 
1300
        // Perform a second move as some issues were only seen on the second move
1301
        $newsection = get_fast_modinfo($course)->get_section_info(2);
1302
        $oldsectionid = $cm->section;
1303
        moveto_module($cm, $newsection);
1304
 
1305
        $cms = get_fast_modinfo($course)->get_cms();
1306
        $cm = reset($cms);
1307
 
1308
        // Check that the cached modinfo contains the correct section info
1309
        $modinfo = get_fast_modinfo($course);
1310
        $this->assertTrue(empty($modinfo->sections[0]));
1311
        $this->assertFalse(empty($modinfo->sections[2]));
1312
 
1313
        // Check that the old section's sequence no longer contains this ID
1314
        $oldsection = $DB->get_record('course_sections', array('id' => $oldsectionid));
1315
        $oldsequences = explode(',', $newsection->sequence);
1316
        $this->assertFalse(in_array($cm->id, $oldsequences));
1317
 
1318
        // Check that the new section's sequence now contains this ID
1319
        $newsection = $DB->get_record('course_sections', array('id' => $newsection->id));
1320
        $newsequences = explode(',', $newsection->sequence);
1321
        $this->assertTrue(in_array($cm->id, $newsequences));
1322
    }
1323
 
1324
    public function test_module_visibility() {
1325
        $this->setAdminUser();
1326
        $this->resetAfterTest(true);
1327
 
1328
        // Create course and modules.
1329
        $course = $this->getDataGenerator()->create_course(array('numsections' => 5));
1330
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
1331
        $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(), 'course' => $course->id));
1332
        $modules = compact('forum', 'assign');
1333
 
1334
        // Hiding the modules.
1335
        foreach ($modules as $mod) {
1336
            set_coursemodule_visible($mod->cmid, 0);
1337
            $this->check_module_visibility($mod, 0, 0);
1338
        }
1339
 
1340
        // Showing the modules.
1341
        foreach ($modules as $mod) {
1342
            set_coursemodule_visible($mod->cmid, 1);
1343
            $this->check_module_visibility($mod, 1, 1);
1344
        }
1345
    }
1346
 
1347
    /**
1348
     * Test rebuildcache = false behaviour.
1349
     *
1350
     * When we pass rebuildcache = false to set_coursemodule_visible, the corusemodinfo cache will still contain
1351
     * the original visibility until we trigger a rebuild.
1352
     *
1353
     * @return void
1354
     * @covers ::set_coursemodule_visible
1355
     */
1356
    public function test_module_visibility_no_rebuild(): void {
1357
        $this->setAdminUser();
1358
        $this->resetAfterTest(true);
1359
 
1360
        // Create course and modules.
1361
        $course = $this->getDataGenerator()->create_course(['numsections' => 5]);
1362
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1363
        $assign = $this->getDataGenerator()->create_module('assign', ['duedate' => time(), 'course' => $course->id]);
1364
        $modules = compact('forum', 'assign');
1365
 
1366
        // Hiding the modules.
1367
        foreach ($modules as $mod) {
1368
            set_coursemodule_visible($mod->cmid, 0, 1, false);
1369
            // The modinfo cache still has the original visibility until we manually trigger a rebuild.
1370
            $cm = get_fast_modinfo($mod->course)->get_cm($mod->cmid);
1371
            $this->assertEquals(1, $cm->visible);
1372
        }
1373
 
1374
        rebuild_course_cache($course->id);
1375
 
1376
        foreach ($modules as $mod) {
1377
            $this->check_module_visibility($mod, 0, 0);
1378
        }
1379
 
1380
        // Showing the modules.
1381
        foreach ($modules as $mod) {
1382
            set_coursemodule_visible($mod->cmid, 1, 1, false);
1383
            $cm = get_fast_modinfo($mod->course)->get_cm($mod->cmid);
1384
            $this->assertEquals(0, $cm->visible);
1385
        }
1386
 
1387
        rebuild_course_cache($course->id);
1388
 
1389
        foreach ($modules as $mod) {
1390
            $this->check_module_visibility($mod, 1, 1);
1391
        }
1392
    }
1393
 
1394
    public function test_section_visibility_events() {
1395
        $this->setAdminUser();
1396
        $this->resetAfterTest(true);
1397
 
1398
        $course = $this->getDataGenerator()->create_course(array('numsections' => 1), array('createsections' => true));
1399
        $sectionnumber = 1;
1400
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
1401
            array('section' => $sectionnumber));
1402
        $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(),
1403
            'course' => $course->id), array('section' => $sectionnumber));
1404
        $sink = $this->redirectEvents();
1405
        set_section_visible($course->id, $sectionnumber, 0);
1406
        $events = $sink->get_events();
1407
 
1408
        // Extract the number of events related to what we are testing, other events
1409
        // such as course_section_updated could have been triggered.
1410
        $count = 0;
1411
        foreach ($events as $event) {
1412
            if ($event instanceof \core\event\course_module_updated) {
1413
                $count++;
1414
            }
1415
        }
1416
        $this->assertSame(2, $count);
1417
        $sink->close();
1418
    }
1419
 
1420
    public function test_section_visibility() {
1421
        $this->setAdminUser();
1422
        $this->resetAfterTest(true);
1423
 
1424
        // Create course.
1425
        $course = $this->getDataGenerator()->create_course(array('numsections' => 3), array('createsections' => true));
1426
 
1427
        $sink = $this->redirectEvents();
1428
 
1429
        // Testing an empty section.
1430
        $sectionnumber = 1;
1431
        set_section_visible($course->id, $sectionnumber, 0);
1432
        $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1433
        $this->assertEquals($section_info->visible, 0);
1434
        set_section_visible($course->id, $sectionnumber, 1);
1435
        $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1436
        $this->assertEquals($section_info->visible, 1);
1437
 
1438
        // Checking that an event was fired.
1439
        $events = $sink->get_events();
1440
        $this->assertInstanceOf('\core\event\course_section_updated', $events[0]);
1441
        $sink->close();
1442
 
1443
        // Testing a section with visible modules.
1444
        $sectionnumber = 2;
1445
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
1446
                array('section' => $sectionnumber));
1447
        $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(),
1448
                'course' => $course->id), array('section' => $sectionnumber));
1449
        $modules = compact('forum', 'assign');
1450
        set_section_visible($course->id, $sectionnumber, 0);
1451
        $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1452
        $this->assertEquals($section_info->visible, 0);
1453
        foreach ($modules as $mod) {
1454
            $this->check_module_visibility($mod, 0, 1);
1455
        }
1456
        set_section_visible($course->id, $sectionnumber, 1);
1457
        $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1458
        $this->assertEquals($section_info->visible, 1);
1459
        foreach ($modules as $mod) {
1460
            $this->check_module_visibility($mod, 1, 1);
1461
        }
1462
 
1463
        // Testing a section with hidden modules, which should stay hidden.
1464
        $sectionnumber = 3;
1465
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
1466
                array('section' => $sectionnumber));
1467
        $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(),
1468
                'course' => $course->id), array('section' => $sectionnumber));
1469
        $modules = compact('forum', 'assign');
1470
        foreach ($modules as $mod) {
1471
            set_coursemodule_visible($mod->cmid, 0);
1472
            $this->check_module_visibility($mod, 0, 0);
1473
        }
1474
        set_section_visible($course->id, $sectionnumber, 0);
1475
        $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1476
        $this->assertEquals($section_info->visible, 0);
1477
        foreach ($modules as $mod) {
1478
            $this->check_module_visibility($mod, 0, 0);
1479
        }
1480
        set_section_visible($course->id, $sectionnumber, 1);
1481
        $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1482
        $this->assertEquals($section_info->visible, 1);
1483
        foreach ($modules as $mod) {
1484
            $this->check_module_visibility($mod, 0, 0);
1485
        }
1486
    }
1487
 
1488
    /**
1489
     * Helper function to assert that a module has correctly been made visible, or hidden.
1490
     *
1491
     * @param stdClass $mod module information
1492
     * @param int $visibility the current state of the module
1493
     * @param int $visibleold the current state of the visibleold property
1494
     * @return void
1495
     */
1496
    public function check_module_visibility($mod, $visibility, $visibleold) {
1497
        global $DB;
1498
        $cm = get_fast_modinfo($mod->course)->get_cm($mod->cmid);
1499
        $this->assertEquals($visibility, $cm->visible);
1500
        $this->assertEquals($visibleold, $cm->visibleold);
1501
 
1502
        // Check the module grade items.
1503
        $grade_items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $cm->modname,
1504
                'iteminstance' => $cm->instance, 'courseid' => $cm->course));
1505
        if ($grade_items) {
1506
            foreach ($grade_items as $grade_item) {
1507
                if ($visibility) {
1508
                    $this->assertFalse($grade_item->is_hidden(), "$cm->modname grade_item not visible");
1509
                } else {
1510
                    $this->assertTrue($grade_item->is_hidden(), "$cm->modname grade_item not hidden");
1511
                }
1512
            }
1513
        }
1514
 
1515
        // Check the events visibility.
1516
        if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $cm->modname))) {
1517
            foreach ($events as $event) {
1518
                $calevent = new calendar_event($event);
1519
                $this->assertEquals($visibility, $calevent->visible, "$cm->modname calendar_event visibility");
1520
            }
1521
        }
1522
    }
1523
 
1524
    public function test_course_page_type_list() {
1525
        global $DB;
1526
        $this->resetAfterTest(true);
1527
 
1528
        // Create a category.
1529
        $category = new stdClass();
1530
        $category->name = 'Test Category';
1531
 
1532
        $testcategory = $this->getDataGenerator()->create_category($category);
1533
 
1534
        // Create a course.
1535
        $course = new stdClass();
1536
        $course->fullname = 'Apu loves Unit Təsts';
1537
        $course->shortname = 'Spread the lŭve';
1538
        $course->idnumber = '123';
1539
        $course->summary = 'Awesome!';
1540
        $course->summaryformat = FORMAT_PLAIN;
1541
        $course->format = 'topics';
1542
        $course->newsitems = 0;
1543
        $course->numsections = 5;
1544
        $course->category = $testcategory->id;
1545
 
1546
        $testcourse = $this->getDataGenerator()->create_course($course);
1547
 
1548
        // Create contexts.
1549
        $coursecontext = context_course::instance($testcourse->id);
1550
        $parentcontext = $coursecontext->get_parent_context(); // Not actually used.
1551
        $pagetype = 'page-course-x'; // Not used either.
1552
        $pagetypelist = course_page_type_list($pagetype, $parentcontext, $coursecontext);
1553
 
1554
        // Page type lists for normal courses.
1555
        $testpagetypelist1 = array();
1556
        $testpagetypelist1['*'] = 'Any page';
1557
        $testpagetypelist1['course-*'] = 'Any course page';
1558
        $testpagetypelist1['course-view-*'] = 'Any type of course main page';
1559
 
1560
        $this->assertEquals($testpagetypelist1, $pagetypelist);
1561
 
1562
        // Get the context for the front page course.
1563
        $sitecoursecontext = context_course::instance(SITEID);
1564
        $pagetypelist = course_page_type_list($pagetype, $parentcontext, $sitecoursecontext);
1565
 
1566
        // Page type list for the front page course.
1567
        $testpagetypelist2 = array('*' => 'Any page');
1568
        $this->assertEquals($testpagetypelist2, $pagetypelist);
1569
 
1570
        // Make sure that providing no current context to the function doesn't result in an error.
1571
        // Calls made from generate_page_type_patterns() may provide null values.
1572
        $pagetypelist = course_page_type_list($pagetype, null, null);
1573
        $this->assertEquals($pagetypelist, $testpagetypelist1);
1574
    }
1575
 
1576
    public function test_compare_activities_by_time_desc() {
1577
 
1578
        // Let's create some test data.
1579
        $activitiesivities = array();
1580
        $x = new stdClass();
1581
        $x->timestamp = null;
1582
        $activities[] = $x;
1583
 
1584
        $x = new stdClass();
1585
        $x->timestamp = 1;
1586
        $activities[] = $x;
1587
 
1588
        $x = new stdClass();
1589
        $x->timestamp = 3;
1590
        $activities[] = $x;
1591
 
1592
        $x = new stdClass();
1593
        $x->timestamp = 0;
1594
        $activities[] = $x;
1595
 
1596
        $x = new stdClass();
1597
        $x->timestamp = 5;
1598
        $activities[] = $x;
1599
 
1600
        $x = new stdClass();
1601
        $activities[] = $x;
1602
 
1603
        $x = new stdClass();
1604
        $x->timestamp = 5;
1605
        $activities[] = $x;
1606
 
1607
        // Do the sorting.
1608
        usort($activities, 'compare_activities_by_time_desc');
1609
 
1610
        // Let's check the result.
1611
        $last = 10;
1612
        foreach($activities as $activity) {
1613
            if (empty($activity->timestamp)) {
1614
                $activity->timestamp = 0;
1615
            }
1616
            $this->assertLessThanOrEqual($last, $activity->timestamp);
1617
        }
1618
    }
1619
 
1620
    public function test_compare_activities_by_time_asc() {
1621
 
1622
        // Let's create some test data.
1623
        $activities = array();
1624
        $x = new stdClass();
1625
        $x->timestamp = null;
1626
        $activities[] = $x;
1627
 
1628
        $x = new stdClass();
1629
        $x->timestamp = 1;
1630
        $activities[] = $x;
1631
 
1632
        $x = new stdClass();
1633
        $x->timestamp = 3;
1634
        $activities[] = $x;
1635
 
1636
        $x = new stdClass();
1637
        $x->timestamp = 0;
1638
        $activities[] = $x;
1639
 
1640
        $x = new stdClass();
1641
        $x->timestamp = 5;
1642
        $activities[] = $x;
1643
 
1644
        $x = new stdClass();
1645
        $activities[] = $x;
1646
 
1647
        $x = new stdClass();
1648
        $x->timestamp = 5;
1649
        $activities[] = $x;
1650
 
1651
        // Do the sorting.
1652
        usort($activities, 'compare_activities_by_time_asc');
1653
 
1654
        // Let's check the result.
1655
        $last = 0;
1656
        foreach($activities as $activity) {
1657
            if (empty($activity->timestamp)) {
1658
                $activity->timestamp = 0;
1659
            }
1660
            $this->assertGreaterThanOrEqual($last, $activity->timestamp);
1661
        }
1662
    }
1663
 
1664
    /**
1665
     * Tests moving a module between hidden/visible sections and
1666
     * verifies that the course/module visiblity seettings are
1667
     * retained.
1668
     */
1669
    public function test_moveto_module_between_hidden_sections() {
1670
        global $DB;
1671
 
1672
        $this->resetAfterTest(true);
1673
 
1674
        $course = $this->getDataGenerator()->create_course(array('numsections' => 4), array('createsections' => true));
1675
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
1676
        $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
1677
        $quiz= $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
1678
 
1679
        // Set the page as hidden
1680
        set_coursemodule_visible($page->cmid, 0);
1681
 
1682
        // Set sections 3 as hidden.
1683
        set_section_visible($course->id, 3, 0);
1684
 
1685
        $modinfo = get_fast_modinfo($course);
1686
 
1687
        $hiddensection = $modinfo->get_section_info(3);
1688
        // New section is definitely not visible:
1689
        $this->assertEquals($hiddensection->visible, 0);
1690
 
1691
        $forumcm = $modinfo->cms[$forum->cmid];
1692
        $pagecm = $modinfo->cms[$page->cmid];
1693
 
1694
        // Move the forum and the page to a hidden section, make sure moveto_module returns 0 as new visibility state.
1695
        $this->assertEquals(0, moveto_module($forumcm, $hiddensection));
1696
        $this->assertEquals(0, moveto_module($pagecm, $hiddensection));
1697
 
1698
        $modinfo = get_fast_modinfo($course);
1699
 
1700
        // Verify that forum and page have been moved to the hidden section and quiz has not.
1701
        $this->assertContainsEquals($forum->cmid, $modinfo->sections[3]);
1702
        $this->assertContainsEquals($page->cmid, $modinfo->sections[3]);
1703
        $this->assertNotContainsEquals($quiz->cmid, $modinfo->sections[3]);
1704
 
1705
        // Verify that forum has been made invisible.
1706
        $forumcm = $modinfo->cms[$forum->cmid];
1707
        $this->assertEquals($forumcm->visible, 0);
1708
        // Verify that old state has been retained.
1709
        $this->assertEquals($forumcm->visibleold, 1);
1710
 
1711
        // Verify that page has stayed invisible.
1712
        $pagecm = $modinfo->cms[$page->cmid];
1713
        $this->assertEquals($pagecm->visible, 0);
1714
        // Verify that old state has been retained.
1715
        $this->assertEquals($pagecm->visibleold, 0);
1716
 
1717
        // Verify that quiz has been unaffected.
1718
        $quizcm = $modinfo->cms[$quiz->cmid];
1719
        $this->assertEquals($quizcm->visible, 1);
1720
 
1721
        // Move forum and page back to visible section.
1722
        // Make sure the visibility is restored to the original value (visible for forum and hidden for page).
1723
        $visiblesection = $modinfo->get_section_info(2);
1724
        $this->assertEquals(1, moveto_module($forumcm, $visiblesection));
1725
        $this->assertEquals(0, moveto_module($pagecm, $visiblesection));
1726
 
1727
        $modinfo = get_fast_modinfo($course);
1728
 
1729
        // Double check that forum has been made visible.
1730
        $forumcm = $modinfo->cms[$forum->cmid];
1731
        $this->assertEquals($forumcm->visible, 1);
1732
 
1733
        // Double check that page has stayed invisible.
1734
        $pagecm = $modinfo->cms[$page->cmid];
1735
        $this->assertEquals($pagecm->visible, 0);
1736
 
1737
        // Move the page in the same section (this is what mod duplicate does).
1738
        // Visibility of page remains 0.
1739
        $this->assertEquals(0, moveto_module($pagecm, $visiblesection, $forumcm));
1740
 
1741
        // Double check that the the page is still hidden.
1742
        $modinfo = get_fast_modinfo($course);
1743
        $pagecm = $modinfo->cms[$page->cmid];
1744
        $this->assertEquals($pagecm->visible, 0);
1745
    }
1746
 
1747
    /**
1748
     * Tests moving a module around in the same section. moveto_module()
1749
     * is called this way in modduplicate.
1750
     */
1751
    public function test_moveto_module_in_same_section() {
1752
        global $DB;
1753
 
1754
        $this->resetAfterTest(true);
1755
 
1756
        $course = $this->getDataGenerator()->create_course(array('numsections' => 3), array('createsections' => true));
1757
        $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
1758
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
1759
 
1760
        // Simulate inconsistent visible/visibleold values (MDL-38713).
1761
        $cm = $DB->get_record('course_modules', array('id' => $page->cmid), '*', MUST_EXIST);
1762
        $cm->visible = 0;
1763
        $cm->visibleold = 1;
1764
        $DB->update_record('course_modules', $cm);
1765
 
1766
        $modinfo = get_fast_modinfo($course);
1767
        $forumcm = $modinfo->cms[$forum->cmid];
1768
        $pagecm = $modinfo->cms[$page->cmid];
1769
 
1770
        // Verify that page is hidden.
1771
        $this->assertEquals($pagecm->visible, 0);
1772
 
1773
        // Verify section 0 is where all mods added.
1774
        $section = $modinfo->get_section_info(0);
1775
        $this->assertEquals($section->id, $forumcm->section);
1776
        $this->assertEquals($section->id, $pagecm->section);
1777
 
1778
 
1779
        // Move the page inside the hidden section. Make sure it is hidden.
1780
        $this->assertEquals(0, moveto_module($pagecm, $section, $forumcm));
1781
 
1782
        // Double check that the the page is still hidden.
1783
        $modinfo = get_fast_modinfo($course);
1784
        $pagecm = $modinfo->cms[$page->cmid];
1785
        $this->assertEquals($pagecm->visible, 0);
1786
    }
1787
 
1788
    /**
1789
     * Tests the function that deletes a course module
1790
     *
1791
     * @param string $type The type of module for the test
1792
     * @param array $options The options for the module creation
1793
     * @dataProvider provider_course_delete_module
1794
     */
1795
    public function test_course_delete_module($type, $options) {
1796
        global $DB;
1797
 
1798
        $this->resetAfterTest(true);
1799
        $this->setAdminUser();
1800
 
1801
        // Create course and modules.
1802
        $course = $this->getDataGenerator()->create_course(array('numsections' => 5));
1803
        $options['course'] = $course->id;
1804
 
1805
        // Generate an assignment with due date (will generate a course event).
1806
        $module = $this->getDataGenerator()->create_module($type, $options);
1807
 
1808
        // Get the module context.
1809
        $modcontext = context_module::instance($module->cmid);
1810
 
1811
        $assocblog = $this->create_module_asscociated_blog($course, $modcontext);
1812
 
1813
        // Verify context exists.
1814
        $this->assertInstanceOf('context_module', $modcontext);
1815
 
1816
        // Make module specific messes.
1817
        switch ($type) {
1818
            case 'assign':
1819
                // Add some tags to this assignment.
1820
                core_tag_tag::set_item_tags('mod_assign', 'assign', $module->id, $modcontext, array('Tag 1', 'Tag 2', 'Tag 3'));
1821
                core_tag_tag::set_item_tags('core', 'course_modules', $module->cmid, $modcontext, array('Tag 3', 'Tag 4', 'Tag 5'));
1822
 
1823
                // Confirm the tag instances were added.
1824
                $criteria = array('component' => 'mod_assign', 'itemtype' => 'assign', 'contextid' => $modcontext->id);
1825
                $this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
1826
                $criteria = array('component' => 'core', 'itemtype' => 'course_modules', 'contextid' => $modcontext->id);
1827
                $this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
1828
 
1829
                // Verify event assignment event has been generated.
1830
                $eventcount = $DB->count_records('event', array('instance' => $module->id, 'modulename' => $type));
1831
                $this->assertEquals(1, $eventcount);
1832
 
1833
                break;
1834
            case 'quiz':
1835
                $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
1836
                $qcat = $qgen->create_question_category(array('contextid' => $modcontext->id));
1837
                $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
1838
                $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
1839
                break;
1840
            default:
1841
                break;
1842
        }
1843
 
1844
        // Run delete..
1845
        course_delete_module($module->cmid);
1846
 
1847
        // Verify the context has been removed.
1848
        $this->assertFalse(context_module::instance($module->cmid, IGNORE_MISSING));
1849
 
1850
        // Verify the course_module record has been deleted.
1851
        $cmcount = $DB->count_records('course_modules', array('id' => $module->cmid));
1852
        $this->assertEmpty($cmcount);
1853
 
1854
        // Verify the blog_association record has been deleted.
1855
        $this->assertCount(0, $DB->get_records('blog_association',
1856
                array('contextid' => $modcontext->id)));
1857
 
1858
        // Verify the blog post record has been deleted.
1859
        $this->assertCount(0, $DB->get_records('post',
1860
                array('id' => $assocblog->id)));
1861
 
1862
        // Verify the tag instance record has been deleted.
1863
        $this->assertCount(0, $DB->get_records('tag_instance',
1864
                array('itemid' => $assocblog->id)));
1865
 
1866
        // Test clean up of module specific messes.
1867
        switch ($type) {
1868
            case 'assign':
1869
                // Verify event assignment events have been removed.
1870
                $eventcount = $DB->count_records('event', array('instance' => $module->id, 'modulename' => $type));
1871
                $this->assertEmpty($eventcount);
1872
 
1873
                // Verify the tag instances were deleted.
1874
                $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id);
1875
                $this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
1876
 
1877
                $criteria = array('component' => 'core', 'itemtype' => 'course_modules', 'contextid' => $modcontext->id);
1878
                $this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
1879
                break;
1880
            case 'quiz':
1881
                // Verify category deleted.
1882
                $criteria = array('contextid' => $modcontext->id);
1883
                $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
1884
 
1885
                // Verify questions deleted.
1886
                $criteria = [$qcat->id];
1887
                $sql = 'SELECT COUNT(q.id)
1888
                          FROM {question} q
1889
                          JOIN {question_versions} qv ON qv.questionid = q.id
1890
                          JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
1891
                          WHERE qbe.questioncategoryid = ?';
1892
                $this->assertEquals(0, $DB->count_records_sql($sql, $criteria));
1893
                break;
1894
            default:
1895
                break;
1896
        }
1897
    }
1898
 
1899
    /**
1900
     * Test that triggering a course_created event works as expected.
1901
     */
1902
    public function test_course_created_event() {
1903
        global $DB;
1904
 
1905
        $this->resetAfterTest();
1906
 
1907
        // Catch the events.
1908
        $sink = $this->redirectEvents();
1909
 
1910
        // Create the course with an id number which is used later when generating a course via the imsenterprise plugin.
1911
        $data = new stdClass();
1912
        $data->idnumber = 'idnumber';
1913
        $course = $this->getDataGenerator()->create_course($data);
1914
        // Get course from DB for comparison.
1915
        $course = $DB->get_record('course', array('id' => $course->id));
1916
 
1917
        // Capture the event.
1918
        $events = $sink->get_events();
1919
        $sink->close();
1920
 
1921
        // Validate the event.
1922
        $event = $events[0];
1923
        $this->assertInstanceOf('\core\event\course_created', $event);
1924
        $this->assertEquals('course', $event->objecttable);
1925
        $this->assertEquals($course->id, $event->objectid);
1926
        $this->assertEquals(context_course::instance($course->id), $event->get_context());
1927
        $this->assertEquals($course, $event->get_record_snapshot('course', $course->id));
1928
 
1929
        // Now we want to trigger creating a course via the imsenterprise.
1930
        // Delete the course we created earlier, as we want the imsenterprise plugin to create this.
1931
        // We do not want print out any of the text this function generates while doing this, which is why
1932
        // we are using ob_start() and ob_end_clean().
1933
        ob_start();
1934
        delete_course($course);
1935
        ob_end_clean();
1936
 
1937
        // Create the XML file we want to use.
1938
        $course->category = (array)$course->category;
1939
        $imstestcase = new imsenterprise_test();
1940
        $imstestcase->imsplugin = enrol_get_plugin('imsenterprise');
1941
        $imstestcase->set_test_config();
1942
        $imstestcase->set_xml_file(false, array($course));
1943
 
1944
        // Capture the event.
1945
        $sink = $this->redirectEvents();
1946
        $imstestcase->imsplugin->cron();
1947
        $events = $sink->get_events();
1948
        $sink->close();
1949
        $event = null;
1950
        foreach ($events as $eventinfo) {
1951
            if ($eventinfo instanceof \core\event\course_created ) {
1952
                $event = $eventinfo;
1953
                break;
1954
            }
1955
        }
1956
 
1957
        // Validate the event triggered is \core\event\course_created. There is no need to validate the other values
1958
        // as they have already been validated in the previous steps. Here we only want to make sure that when the
1959
        // imsenterprise plugin creates a course an event is triggered.
1960
        $this->assertInstanceOf('\core\event\course_created', $event);
1961
        $this->assertEventContextNotUsed($event);
1962
    }
1963
 
1964
    /**
1965
     * Test that triggering a course_updated event works as expected.
1966
     */
1967
    public function test_course_updated_event() {
1968
        global $DB;
1969
 
1970
        $this->resetAfterTest();
1971
 
1972
        // Create a course.
1973
        $course = $this->getDataGenerator()->create_course();
1974
 
1975
        // Create a category we are going to move this course to.
1976
        $category = $this->getDataGenerator()->create_category();
1977
 
1978
        // Create a hidden category we are going to move this course to.
1979
        $categoryhidden = $this->getDataGenerator()->create_category(array('visible' => 0));
1980
 
1981
        // Update course and catch course_updated event.
1982
        $sink = $this->redirectEvents();
1983
        update_course($course);
1984
        $events = $sink->get_events();
1985
        $sink->close();
1986
 
1987
        // Get updated course information from the DB.
1988
        $updatedcourse = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
1989
        // Validate event.
1990
        $event = array_shift($events);
1991
        $this->assertInstanceOf('\core\event\course_updated', $event);
1992
        $this->assertEquals('course', $event->objecttable);
1993
        $this->assertEquals($updatedcourse->id, $event->objectid);
1994
        $this->assertEquals(context_course::instance($course->id), $event->get_context());
1995
        $url = new moodle_url('/course/edit.php', array('id' => $event->objectid));
1996
        $this->assertEquals($url, $event->get_url());
1997
        $this->assertEquals($updatedcourse, $event->get_record_snapshot('course', $event->objectid));
1998
 
1999
        // Move course and catch course_updated event.
2000
        $sink = $this->redirectEvents();
2001
        move_courses(array($course->id), $category->id);
2002
        $events = $sink->get_events();
2003
        $sink->close();
2004
 
2005
        // Return the moved course information from the DB.
2006
        $movedcourse = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
2007
        // Validate event.
2008
        $event = array_shift($events);
2009
        $this->assertInstanceOf('\core\event\course_updated', $event);
2010
        $this->assertEquals('course', $event->objecttable);
2011
        $this->assertEquals($movedcourse->id, $event->objectid);
2012
        $this->assertEquals(context_course::instance($course->id), $event->get_context());
2013
        $this->assertEquals($movedcourse, $event->get_record_snapshot('course', $movedcourse->id));
2014
 
2015
        // Move course to hidden category and catch course_updated event.
2016
        $sink = $this->redirectEvents();
2017
        move_courses(array($course->id), $categoryhidden->id);
2018
        $events = $sink->get_events();
2019
        $sink->close();
2020
 
2021
        // Return the moved course information from the DB.
2022
        $movedcoursehidden = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
2023
        // Validate event.
2024
        $event = array_shift($events);
2025
        $this->assertInstanceOf('\core\event\course_updated', $event);
2026
        $this->assertEquals('course', $event->objecttable);
2027
        $this->assertEquals($movedcoursehidden->id, $event->objectid);
2028
        $this->assertEquals(context_course::instance($course->id), $event->get_context());
2029
        $this->assertEquals($movedcoursehidden, $event->get_record_snapshot('course', $movedcoursehidden->id));
2030
        $this->assertEventContextNotUsed($event);
2031
    }
2032
 
2033
    /**
2034
     * Test that triggering a course_updated event logs changes.
2035
     */
2036
    public function test_course_updated_event_with_changes() {
2037
        global $DB;
2038
 
2039
        $this->resetAfterTest();
2040
 
2041
        // Create a course.
2042
        $course = $this->getDataGenerator()->create_course((object)['visible' => 1]);
2043
 
2044
        $editedcourse = $DB->get_record('course', ['id' => $course->id]);
2045
        $editedcourse->visible = 0;
2046
 
2047
        // Update course and catch course_updated event.
2048
        $sink = $this->redirectEvents();
2049
        update_course($editedcourse);
2050
        $events = $sink->get_events();
2051
        $sink->close();
2052
 
2053
        $event = array_shift($events);
2054
        $this->assertInstanceOf('\core\event\course_updated', $event);
2055
        $otherdata = [
2056
            'shortname' => $course->shortname,
2057
            'fullname' => $course->fullname,
2058
            'updatedfields' => [
2059
                'visible' => 0
2060
            ]
2061
        ];
2062
        $this->assertEquals($otherdata, $event->other);
2063
 
2064
    }
2065
 
2066
    /**
2067
     * Test that triggering a course_deleted event works as expected.
2068
     */
2069
    public function test_course_deleted_event() {
2070
        $this->resetAfterTest();
2071
 
2072
        // Create the course.
2073
        $course = $this->getDataGenerator()->create_course();
2074
 
2075
        // Save the course context before we delete the course.
2076
        $coursecontext = context_course::instance($course->id);
2077
 
2078
        // Catch the update event.
2079
        $sink = $this->redirectEvents();
2080
 
2081
        // Call delete_course() which will trigger the course_deleted event and the course_content_deleted
2082
        // event. This function prints out data to the screen, which we do not want during a PHPUnit test,
2083
        // so use ob_start and ob_end_clean to prevent this.
2084
        ob_start();
2085
        delete_course($course);
2086
        ob_end_clean();
2087
 
2088
        // Capture the event.
2089
        $events = $sink->get_events();
2090
        $sink->close();
2091
 
2092
        // Validate the event.
2093
        $event = array_pop($events);
2094
        $this->assertInstanceOf('\core\event\course_deleted', $event);
2095
        $this->assertEquals('course', $event->objecttable);
2096
        $this->assertEquals($course->id, $event->objectid);
2097
        $this->assertEquals($coursecontext->id, $event->contextid);
2098
        $this->assertEquals($course, $event->get_record_snapshot('course', $course->id));
2099
        $eventdata = $event->get_data();
2100
        $this->assertSame($course->idnumber, $eventdata['other']['idnumber']);
2101
        $this->assertSame($course->fullname, $eventdata['other']['fullname']);
2102
        $this->assertSame($course->shortname, $eventdata['other']['shortname']);
2103
 
2104
        $this->assertEventContextNotUsed($event);
2105
    }
2106
 
2107
    /**
2108
     * Test that triggering a course_content_deleted event works as expected.
2109
     */
2110
    public function test_course_content_deleted_event() {
2111
        global $DB;
2112
 
2113
        $this->resetAfterTest();
2114
 
2115
        // Create the course.
2116
        $course = $this->getDataGenerator()->create_course();
2117
 
2118
        // Get the course from the DB. The data generator adds some extra properties, such as
2119
        // numsections, to the course object which will fail the assertions later on.
2120
        $course = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
2121
 
2122
        // Save the course context before we delete the course.
2123
        $coursecontext = context_course::instance($course->id);
2124
 
2125
        // Catch the update event.
2126
        $sink = $this->redirectEvents();
2127
 
2128
        remove_course_contents($course->id, false);
2129
 
2130
        // Capture the event.
2131
        $events = $sink->get_events();
2132
        $sink->close();
2133
 
2134
        // Validate the event.
2135
        $event = array_pop($events);
2136
        $this->assertInstanceOf('\core\event\course_content_deleted', $event);
2137
        $this->assertEquals('course', $event->objecttable);
2138
        $this->assertEquals($course->id, $event->objectid);
2139
        $this->assertEquals($coursecontext->id, $event->contextid);
2140
        $this->assertEquals($course, $event->get_record_snapshot('course', $course->id));
2141
        $this->assertEventContextNotUsed($event);
2142
    }
2143
 
2144
    /**
2145
     * Test that triggering a course_category_deleted event works as expected.
2146
     */
2147
    public function test_course_category_deleted_event() {
2148
        $this->resetAfterTest();
2149
 
2150
        // Create a category.
2151
        $category = $this->getDataGenerator()->create_category();
2152
 
2153
        // Save the original record/context before it is deleted.
2154
        $categoryrecord = $category->get_db_record();
2155
        $categorycontext = context_coursecat::instance($category->id);
2156
 
2157
        // Catch the update event.
2158
        $sink = $this->redirectEvents();
2159
 
2160
        // Delete the category.
2161
        $category->delete_full();
2162
 
2163
        // Capture the event.
2164
        $events = $sink->get_events();
2165
        $sink->close();
2166
 
2167
        // Validate the event.
2168
        $event = $events[0];
2169
        $this->assertInstanceOf('\core\event\course_category_deleted', $event);
2170
        $this->assertEquals('course_categories', $event->objecttable);
2171
        $this->assertEquals($category->id, $event->objectid);
2172
        $this->assertEquals($categorycontext->id, $event->contextid);
2173
        $this->assertEquals([
2174
            'name' => $category->name,
2175
        ], $event->other);
2176
        $this->assertEquals($categoryrecord, $event->get_record_snapshot($event->objecttable, $event->objectid));
2177
        $this->assertEquals(null, $event->get_url());
2178
 
2179
        // Create two categories.
2180
        $category = $this->getDataGenerator()->create_category();
2181
        $category2 = $this->getDataGenerator()->create_category();
2182
 
2183
        // Save the original record/context before it is moved and then deleted.
2184
        $category2record = $category2->get_db_record();
2185
        $category2context = context_coursecat::instance($category2->id);
2186
 
2187
        // Catch the update event.
2188
        $sink = $this->redirectEvents();
2189
 
2190
        // Move the category.
2191
        $category2->delete_move($category->id);
2192
 
2193
        // Capture the event.
2194
        $events = $sink->get_events();
2195
        $sink->close();
2196
 
2197
        // Validate the event.
2198
        $event = $events[0];
2199
        $this->assertInstanceOf('\core\event\course_category_deleted', $event);
2200
        $this->assertEquals('course_categories', $event->objecttable);
2201
        $this->assertEquals($category2->id, $event->objectid);
2202
        $this->assertEquals($category2context->id, $event->contextid);
2203
        $this->assertEquals([
2204
            'name' => $category2->name,
2205
            'contentmovedcategoryid' => $category->id,
2206
        ], $event->other);
2207
        $this->assertEquals($category2record, $event->get_record_snapshot($event->objecttable, $event->objectid));
2208
        $this->assertEventContextNotUsed($event);
2209
    }
2210
 
2211
    /**
2212
     * Test that triggering a course_backup_created event works as expected.
2213
     */
2214
    public function test_course_backup_created_event() {
2215
        global $CFG;
2216
 
2217
        // Get the necessary files to perform backup and restore.
2218
        require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
2219
        require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
2220
 
2221
        $this->resetAfterTest();
2222
 
2223
        // Set to admin user.
2224
        $this->setAdminUser();
2225
 
2226
        // The user id is going to be 2 since we are the admin user.
2227
        $userid = 2;
2228
 
2229
        // Create a course.
2230
        $course = $this->getDataGenerator()->create_course();
2231
 
2232
        // Create backup file and save it to the backup location.
2233
        $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
2234
            backup::INTERACTIVE_NO, backup::MODE_GENERAL, $userid);
2235
        $sink = $this->redirectEvents();
2236
        $bc->execute_plan();
2237
 
2238
        // Capture the event.
2239
        $events = $sink->get_events();
2240
        $sink->close();
2241
 
2242
        // Validate the event.
2243
        $event = array_pop($events);
2244
        $this->assertInstanceOf('\core\event\course_backup_created', $event);
2245
        $this->assertEquals('course', $event->objecttable);
2246
        $this->assertEquals($bc->get_courseid(), $event->objectid);
2247
        $this->assertEquals(context_course::instance($bc->get_courseid())->id, $event->contextid);
2248
 
2249
        $url = new moodle_url('/course/view.php', array('id' => $event->objectid));
2250
        $this->assertEquals($url, $event->get_url());
2251
        $this->assertEventContextNotUsed($event);
2252
 
2253
        // Destroy the resource controller since we are done using it.
2254
        $bc->destroy();
2255
    }
2256
 
2257
    /**
2258
     * Test that triggering a course_restored event works as expected.
2259
     */
2260
    public function test_course_restored_event() {
2261
        global $CFG;
2262
 
2263
        // Get the necessary files to perform backup and restore.
2264
        require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
2265
        require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
2266
 
2267
        $this->resetAfterTest();
2268
 
2269
        // Set to admin user.
2270
        $this->setAdminUser();
2271
 
2272
        // The user id is going to be 2 since we are the admin user.
2273
        $userid = 2;
2274
 
2275
        // Create a course.
2276
        $course = $this->getDataGenerator()->create_course();
2277
 
2278
        // Create backup file and save it to the backup location.
2279
        $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
2280
            backup::INTERACTIVE_NO, backup::MODE_GENERAL, $userid);
2281
        $bc->execute_plan();
2282
        $results = $bc->get_results();
2283
        $file = $results['backup_destination'];
2284
        $fp = get_file_packer('application/vnd.moodle.backup');
2285
        $filepath = $CFG->dataroot . '/temp/backup/test-restore-course-event';
2286
        $file->extract_to_pathname($fp, $filepath);
2287
        $bc->destroy();
2288
 
2289
        // Now we want to catch the restore course event.
2290
        $sink = $this->redirectEvents();
2291
 
2292
        // Now restore the course to trigger the event.
2293
        $rc = new restore_controller('test-restore-course-event', $course->id, backup::INTERACTIVE_NO,
2294
            backup::MODE_GENERAL, $userid, backup::TARGET_NEW_COURSE);
2295
        $rc->execute_precheck();
2296
        $rc->execute_plan();
2297
 
2298
        // Capture the event.
2299
        $events = $sink->get_events();
2300
        $sink->close();
2301
 
2302
        // Validate the event.
2303
        $event = array_pop($events);
2304
        $this->assertInstanceOf('\core\event\course_restored', $event);
2305
        $this->assertEquals('course', $event->objecttable);
2306
        $this->assertEquals($rc->get_courseid(), $event->objectid);
2307
        $this->assertEquals(context_course::instance($rc->get_courseid())->id, $event->contextid);
2308
        $this->assertEventContextNotUsed($event);
2309
 
2310
        // Destroy the resource controller since we are done using it.
2311
        $rc->destroy();
2312
    }
2313
 
2314
    /**
2315
     * Test that triggering a course_section_updated event works as expected.
2316
     */
2317
    public function test_course_section_updated_event() {
2318
        global $DB;
2319
 
2320
        $this->resetAfterTest();
2321
 
2322
        // Create the course with sections.
2323
        $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
2324
        $sections = $DB->get_records('course_sections', array('course' => $course->id));
2325
 
2326
        $coursecontext = context_course::instance($course->id);
2327
 
2328
        $section = array_pop($sections);
2329
        $section->name = 'Test section';
2330
        $section->summary = 'Test section summary';
2331
        $DB->update_record('course_sections', $section);
2332
 
2333
        // Trigger an event for course section update.
2334
        $event = \core\event\course_section_updated::create(
2335
                array(
2336
                    'objectid' => $section->id,
2337
                    'courseid' => $course->id,
2338
                    'context' => context_course::instance($course->id),
2339
                    'other' => array(
2340
                        'sectionnum' => $section->section
2341
                    )
2342
                )
2343
            );
2344
        $event->add_record_snapshot('course_sections', $section);
2345
        // Trigger and catch event.
2346
        $sink = $this->redirectEvents();
2347
        $event->trigger();
2348
        $events = $sink->get_events();
2349
        $sink->close();
2350
 
2351
        // Validate the event.
2352
        $event = $events[0];
2353
        $this->assertInstanceOf('\core\event\course_section_updated', $event);
2354
        $this->assertEquals('course_sections', $event->objecttable);
2355
        $this->assertEquals($section->id, $event->objectid);
2356
        $this->assertEquals($course->id, $event->courseid);
2357
        $this->assertEquals($coursecontext->id, $event->contextid);
2358
        $this->assertEquals($section->section, $event->other['sectionnum']);
2359
        $expecteddesc = "The user with id '{$event->userid}' updated section number '{$event->other['sectionnum']}' for the course with id '{$event->courseid}'";
2360
        $this->assertEquals($expecteddesc, $event->get_description());
2361
        $url = new moodle_url('/course/editsection.php', array('id' => $event->objectid));
2362
        $this->assertEquals($url, $event->get_url());
2363
        $this->assertEquals($section, $event->get_record_snapshot('course_sections', $event->objectid));
2364
        $id = $section->id;
2365
        $sectionnum = $section->section;
2366
        $this->assertEventContextNotUsed($event);
2367
    }
2368
 
2369
    /**
2370
     * Test that triggering a course_section_deleted event works as expected.
2371
     */
2372
    public function test_course_section_deleted_event() {
2373
        global $USER, $DB;
2374
        $this->resetAfterTest();
2375
        $sink = $this->redirectEvents();
2376
 
2377
        // Create the course with sections.
2378
        $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
2379
        $sections = $DB->get_records('course_sections', array('course' => $course->id), 'section');
2380
        $coursecontext = context_course::instance($course->id);
2381
        $section = array_pop($sections);
2382
        course_delete_section($course, $section);
2383
        $events = $sink->get_events();
2384
        $event = array_pop($events); // Delete section event.
2385
        $sink->close();
2386
 
2387
        // Validate event data.
2388
        $this->assertInstanceOf('\core\event\course_section_deleted', $event);
2389
        $this->assertEquals('course_sections', $event->objecttable);
2390
        $this->assertEquals($section->id, $event->objectid);
2391
        $this->assertEquals($course->id, $event->courseid);
2392
        $this->assertEquals($coursecontext->id, $event->contextid);
2393
        $this->assertEquals($section->section, $event->other['sectionnum']);
2394
        $expecteddesc = "The user with id '{$event->userid}' deleted section number '{$event->other['sectionnum']}' " .
2395
                "(section name '{$event->other['sectionname']}') for the course with id '{$event->courseid}'";
2396
        $this->assertEquals($expecteddesc, $event->get_description());
2397
        $this->assertEquals($section, $event->get_record_snapshot('course_sections', $event->objectid));
2398
        $this->assertNull($event->get_url());
2399
        $this->assertEventContextNotUsed($event);
2400
    }
2401
 
2402
    public function test_course_integrity_check() {
2403
        global $DB;
2404
 
2405
        $this->resetAfterTest(true);
2406
        $course = $this->getDataGenerator()->create_course(array('numsections' => 1),
2407
           array('createsections'=>true));
2408
 
2409
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
2410
                array('section' => 0));
2411
        $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id),
2412
                array('section' => 0));
2413
        $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id),
2414
                array('section' => 0));
2415
        $correctseq = join(',', array($forum->cmid, $page->cmid, $quiz->cmid));
2416
 
2417
        $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2418
        $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2419
        $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2420
        $this->assertEquals($correctseq, $section0->sequence);
2421
        $this->assertEmpty($section1->sequence);
2422
        $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2423
        $this->assertEquals($section0->id, $cms[$page->cmid]->section);
2424
        $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2425
        $this->assertEmpty(course_integrity_check($course->id));
2426
 
2427
        // Now let's make manual change in DB and let course_integrity_check() fix it:
2428
 
2429
        // 1. Module appears twice in one section.
2430
        $DB->update_record('course_sections', array('id' => $section0->id, 'sequence' => $section0->sequence. ','. $page->cmid));
2431
        $this->assertEquals(
2432
                array('Failed integrity check for course ['. $course->id.
2433
                ']. Sequence for course section ['. $section0->id. '] is "'.
2434
                $section0->sequence. ','. $page->cmid. '", must be "'.
2435
                $section0->sequence. '"'),
2436
                course_integrity_check($course->id));
2437
        $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2438
        $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2439
        $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2440
        $this->assertEquals($correctseq, $section0->sequence);
2441
        $this->assertEmpty($section1->sequence);
2442
        $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2443
        $this->assertEquals($section0->id, $cms[$page->cmid]->section);
2444
        $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2445
 
2446
        // 2. Module appears in two sections (last section wins).
2447
        $DB->update_record('course_sections', array('id' => $section1->id, 'sequence' => ''. $page->cmid));
2448
        // First message about double mentioning in sequence, second message about wrong section field for $page.
2449
        $this->assertEquals(array(
2450
            'Failed integrity check for course ['. $course->id. ']. Course module ['. $page->cmid.
2451
            '] must be removed from sequence of section ['. $section0->id.
2452
            '] because it is also present in sequence of section ['. $section1->id. ']',
2453
            'Failed integrity check for course ['. $course->id. ']. Course module ['. $page->cmid.
2454
            '] points to section ['. $section0->id. '] instead of ['. $section1->id. ']'),
2455
                course_integrity_check($course->id));
2456
        $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2457
        $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2458
        $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2459
        $this->assertEquals($forum->cmid. ','. $quiz->cmid, $section0->sequence);
2460
        $this->assertEquals(''. $page->cmid, $section1->sequence);
2461
        $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2462
        $this->assertEquals($section1->id, $cms[$page->cmid]->section);
2463
        $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2464
 
2465
        // 3. Module id is not present in course_section.sequence (integrity check with $fullcheck = false).
2466
        $DB->update_record('course_sections', array('id' => $section1->id, 'sequence' => ''));
2467
        $this->assertEmpty(course_integrity_check($course->id)); // Not an error!
2468
        $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2469
        $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2470
        $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2471
        $this->assertEquals($forum->cmid. ','. $quiz->cmid, $section0->sequence);
2472
        $this->assertEmpty($section1->sequence);
2473
        $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2474
        $this->assertEquals($section1->id, $cms[$page->cmid]->section); // Not changed.
2475
        $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2476
 
2477
        // 4. Module id is not present in course_section.sequence (integrity check with $fullcheck = true).
2478
        $this->assertEquals(array('Failed integrity check for course ['. $course->id. ']. Course module ['.
2479
                $page->cmid. '] is missing from sequence of section ['. $section1->id. ']'),
2480
                course_integrity_check($course->id, null, null, true)); // Error!
2481
        $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2482
        $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2483
        $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2484
        $this->assertEquals($forum->cmid. ','. $quiz->cmid, $section0->sequence);
2485
        $this->assertEquals(''. $page->cmid, $section1->sequence);  // Yay, module added to section.
2486
        $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2487
        $this->assertEquals($section1->id, $cms[$page->cmid]->section); // Not changed.
2488
        $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2489
 
2490
        // 5. Module id is not present in course_section.sequence and it's section is invalid (integrity check with $fullcheck = true).
2491
        $DB->update_record('course_modules', array('id' => $page->cmid, 'section' => 8765));
2492
        $DB->update_record('course_sections', array('id' => $section1->id, 'sequence' => ''));
2493
        $this->assertEquals(array(
2494
            'Failed integrity check for course ['. $course->id. ']. Course module ['. $page->cmid.
2495
            '] is missing from sequence of section ['. $section0->id. ']',
2496
            'Failed integrity check for course ['. $course->id. ']. Course module ['. $page->cmid.
2497
            '] points to section [8765] instead of ['. $section0->id. ']'),
2498
                course_integrity_check($course->id, null, null, true));
2499
        $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2500
        $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2501
        $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2502
        $this->assertEquals($forum->cmid. ','. $quiz->cmid. ','. $page->cmid, $section0->sequence); // Module added to section.
2503
        $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2504
        $this->assertEquals($section0->id, $cms[$page->cmid]->section); // Section changed to section0.
2505
        $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2506
 
2507
        // 6. Module is deleted from course_modules but not deleted in sequence (integrity check with $fullcheck = true).
2508
        $DB->delete_records('course_modules', array('id' => $page->cmid));
2509
        $this->assertEquals(array('Failed integrity check for course ['. $course->id. ']. Course module ['.
2510
                $page->cmid. '] does not exist but is present in the sequence of section ['. $section0->id. ']'),
2511
                course_integrity_check($course->id, null, null, true));
2512
        $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2513
        $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2514
        $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2515
        $this->assertEquals($forum->cmid. ','. $quiz->cmid, $section0->sequence);
2516
        $this->assertEmpty($section1->sequence);
2517
        $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2518
        $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2519
        $this->assertEquals(2, count($cms));
2520
    }
2521
 
2522
    /**
2523
     * Tests for event related to course module creation.
2524
     */
2525
    public function test_course_module_created_event() {
2526
        global $USER;
2527
 
2528
        $this->resetAfterTest();
2529
        $this->setAdminUser();
2530
 
2531
        // Create an assign module.
2532
        $sink = $this->redirectEvents();
2533
        $course = $this->getDataGenerator()->create_course();
2534
        $module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
2535
        $events = $sink->get_events();
2536
        $eventscount = 0;
2537
 
2538
        // Validate event data.
2539
        foreach ($events as $event) {
2540
            if ($event instanceof \core\event\course_module_created) {
2541
                $eventscount++;
2542
 
2543
                $this->assertEquals($module->cmid, $event->objectid);
2544
                $this->assertEquals($USER->id, $event->userid);
2545
                $this->assertEquals('course_modules', $event->objecttable);
2546
                $url = new moodle_url('/mod/assign/view.php', array('id' => $module->cmid));
2547
                $this->assertEquals($url, $event->get_url());
2548
                $this->assertEventContextNotUsed($event);
2549
            }
2550
        }
2551
        // Only one \core\event\course_module_created event should be triggered.
2552
        $this->assertEquals(1, $eventscount);
2553
 
2554
        // Let us see if duplicating an activity results in a nice course module created event.
2555
        $sink->clear();
2556
        $course = get_course($module->course);
2557
        $cm = get_coursemodule_from_id('assign', $module->cmid, 0, false, MUST_EXIST);
2558
        $newcm = duplicate_module($course, $cm);
2559
        $events = $sink->get_events();
2560
        $eventscount = 0;
2561
        $sink->close();
2562
 
2563
        foreach ($events as $event) {
2564
            if ($event instanceof \core\event\course_module_created) {
2565
                $eventscount++;
2566
                // Validate event data.
2567
                $this->assertInstanceOf('\core\event\course_module_created', $event);
2568
                $this->assertEquals($newcm->id, $event->objectid);
2569
                $this->assertEquals($USER->id, $event->userid);
2570
                $this->assertEquals($course->id, $event->courseid);
2571
                $url = new moodle_url('/mod/assign/view.php', array('id' => $newcm->id));
2572
                $this->assertEquals($url, $event->get_url());
2573
            }
2574
        }
2575
 
2576
        // Only one \core\event\course_module_created event should be triggered.
2577
        $this->assertEquals(1, $eventscount);
2578
    }
2579
 
2580
    /**
2581
     * Tests for event validations related to course module creation.
2582
     */
2583
    public function test_course_module_created_event_exceptions() {
2584
 
2585
        $this->resetAfterTest();
2586
 
2587
        // Generate data.
2588
        $modinfo = $this->create_specific_module_test('assign');
2589
        $context = context_module::instance($modinfo->coursemodule);
2590
 
2591
        // Test not setting instanceid.
2592
        try {
2593
            $event = \core\event\course_module_created::create(array(
2594
                'courseid' => $modinfo->course,
2595
                'context'  => $context,
2596
                'objectid' => $modinfo->coursemodule,
2597
                'other'    => array(
2598
                    'modulename' => 'assign',
2599
                    'name'       => 'My assignment',
2600
                )
2601
            ));
2602
            $this->fail("Event validation should not allow \\core\\event\\course_module_created to be triggered without
2603
                    other['instanceid']");
2604
        } catch (coding_exception $e) {
2605
            $this->assertStringContainsString("The 'instanceid' value must be set in other.", $e->getMessage());
2606
        }
2607
 
2608
        // Test not setting modulename.
2609
        try {
2610
            $event = \core\event\course_module_created::create(array(
2611
                'courseid' => $modinfo->course,
2612
                'context'  => $context,
2613
                'objectid' => $modinfo->coursemodule,
2614
                'other'    => array(
2615
                    'instanceid' => $modinfo->instance,
2616
                    'name'       => 'My assignment',
2617
                )
2618
            ));
2619
            $this->fail("Event validation should not allow \\core\\event\\course_module_created to be triggered without
2620
                    other['modulename']");
2621
        } catch (coding_exception $e) {
2622
            $this->assertStringContainsString("The 'modulename' value must be set in other.", $e->getMessage());
2623
        }
2624
 
2625
        // Test not setting name.
2626
 
2627
        try {
2628
            $event = \core\event\course_module_created::create(array(
2629
                'courseid' => $modinfo->course,
2630
                'context'  => $context,
2631
                'objectid' => $modinfo->coursemodule,
2632
                'other'    => array(
2633
                    'modulename' => 'assign',
2634
                    'instanceid' => $modinfo->instance,
2635
                )
2636
            ));
2637
            $this->fail("Event validation should not allow \\core\\event\\course_module_created to be triggered without
2638
                    other['name']");
2639
        } catch (coding_exception $e) {
2640
            $this->assertStringContainsString("The 'name' value must be set in other.", $e->getMessage());
2641
        }
2642
 
2643
    }
2644
 
2645
    /**
2646
     * Tests for event related to course module updates.
2647
     */
2648
    public function test_course_module_updated_event() {
2649
        global $USER, $DB;
2650
        $this->resetAfterTest();
2651
 
2652
        // Update a forum module.
2653
        $sink = $this->redirectEvents();
2654
        $modinfo = $this->update_specific_module_test('forum');
2655
        $events = $sink->get_events();
2656
        $eventscount = 0;
2657
        $sink->close();
2658
 
2659
        $cm = $DB->get_record('course_modules', array('id' => $modinfo->coursemodule), '*', MUST_EXIST);
2660
        $mod = $DB->get_record('forum', array('id' => $cm->instance), '*', MUST_EXIST);
2661
 
2662
        // Validate event data.
2663
        foreach ($events as $event) {
2664
            if ($event instanceof \core\event\course_module_updated) {
2665
                $eventscount++;
2666
 
2667
                $this->assertEquals($cm->id, $event->objectid);
2668
                $this->assertEquals($USER->id, $event->userid);
2669
                $this->assertEquals('course_modules', $event->objecttable);
2670
                $url = new moodle_url('/mod/forum/view.php', array('id' => $cm->id));
2671
                $this->assertEquals($url, $event->get_url());
2672
                $this->assertEventContextNotUsed($event);
2673
            }
2674
        }
2675
 
2676
        // Only one \core\event\course_module_updated event should be triggered.
2677
        $this->assertEquals(1, $eventscount);
2678
    }
2679
 
2680
    /**
2681
     * Tests for create_from_cm method.
2682
     */
2683
    public function test_course_module_create_from_cm() {
2684
        $this->resetAfterTest();
2685
        $this->setAdminUser();
2686
 
2687
        // Create course and modules.
2688
        $course = $this->getDataGenerator()->create_course(array('numsections' => 5));
2689
 
2690
        // Generate an assignment.
2691
        $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id));
2692
 
2693
        // Get the module context.
2694
        $modcontext = context_module::instance($assign->cmid);
2695
 
2696
        // Get course module.
2697
        $cm = get_coursemodule_from_id(null, $assign->cmid, $course->id, false, MUST_EXIST);
2698
 
2699
        // Create an event from course module.
2700
        $event = \core\event\course_module_updated::create_from_cm($cm, $modcontext);
2701
 
2702
        // Trigger the events.
2703
        $sink = $this->redirectEvents();
2704
        $event->trigger();
2705
        $events = $sink->get_events();
2706
        $event2 = array_pop($events);
2707
 
2708
        // Test event data.
2709
        $this->assertInstanceOf('\core\event\course_module_updated', $event);
2710
        $this->assertEquals($cm->id, $event2->objectid);
2711
        $this->assertEquals($modcontext, $event2->get_context());
2712
        $this->assertEquals($cm->modname, $event2->other['modulename']);
2713
        $this->assertEquals($cm->instance, $event2->other['instanceid']);
2714
        $this->assertEquals($cm->name, $event2->other['name']);
2715
        $this->assertEventContextNotUsed($event2);
2716
    }
2717
 
2718
    /**
2719
     * Tests for event validations related to course module update.
2720
     */
2721
    public function test_course_module_updated_event_exceptions() {
2722
 
2723
        $this->resetAfterTest();
2724
 
2725
        // Generate data.
2726
        $modinfo = $this->create_specific_module_test('assign');
2727
        $context = context_module::instance($modinfo->coursemodule);
2728
 
2729
        // Test not setting instanceid.
2730
        try {
2731
            $event = \core\event\course_module_updated::create(array(
2732
                'courseid' => $modinfo->course,
2733
                'context'  => $context,
2734
                'objectid' => $modinfo->coursemodule,
2735
                'other'    => array(
2736
                    'modulename' => 'assign',
2737
                    'name'       => 'My assignment',
2738
                )
2739
            ));
2740
            $this->fail("Event validation should not allow \\core\\event\\course_module_updated to be triggered without
2741
                    other['instanceid']");
2742
        } catch (coding_exception $e) {
2743
            $this->assertStringContainsString("The 'instanceid' value must be set in other.", $e->getMessage());
2744
        }
2745
 
2746
        // Test not setting modulename.
2747
        try {
2748
            $event = \core\event\course_module_updated::create(array(
2749
                'courseid' => $modinfo->course,
2750
                'context'  => $context,
2751
                'objectid' => $modinfo->coursemodule,
2752
                'other'    => array(
2753
                    'instanceid' => $modinfo->instance,
2754
                    'name'       => 'My assignment',
2755
                )
2756
            ));
2757
            $this->fail("Event validation should not allow \\core\\event\\course_module_updated to be triggered without
2758
                    other['modulename']");
2759
        } catch (coding_exception $e) {
2760
            $this->assertStringContainsString("The 'modulename' value must be set in other.", $e->getMessage());
2761
        }
2762
 
2763
        // Test not setting name.
2764
 
2765
        try {
2766
            $event = \core\event\course_module_updated::create(array(
2767
                'courseid' => $modinfo->course,
2768
                'context'  => $context,
2769
                'objectid' => $modinfo->coursemodule,
2770
                'other'    => array(
2771
                    'modulename' => 'assign',
2772
                    'instanceid' => $modinfo->instance,
2773
                )
2774
            ));
2775
            $this->fail("Event validation should not allow \\core\\event\\course_module_updated to be triggered without
2776
                    other['name']");
2777
        } catch (coding_exception $e) {
2778
            $this->assertStringContainsString("The 'name' value must be set in other.", $e->getMessage());
2779
        }
2780
 
2781
    }
2782
 
2783
    /**
2784
     * Tests for event related to course module delete.
2785
     */
2786
    public function test_course_module_deleted_event() {
2787
        global $USER, $DB;
2788
        $this->resetAfterTest();
2789
 
2790
        // Create and delete a module.
2791
        $sink = $this->redirectEvents();
2792
        $modinfo = $this->create_specific_module_test('forum');
2793
        $cm = $DB->get_record('course_modules', array('id' => $modinfo->coursemodule), '*', MUST_EXIST);
2794
        course_delete_module($modinfo->coursemodule);
2795
        $events = $sink->get_events();
2796
        $event = array_pop($events); // delete module event.;
2797
        $sink->close();
2798
 
2799
        // Validate event data.
2800
        $this->assertInstanceOf('\core\event\course_module_deleted', $event);
2801
        $this->assertEquals($cm->id, $event->objectid);
2802
        $this->assertEquals($USER->id, $event->userid);
2803
        $this->assertEquals('course_modules', $event->objecttable);
2804
        $this->assertEquals(null, $event->get_url());
2805
        $this->assertEquals($cm, $event->get_record_snapshot('course_modules', $cm->id));
2806
    }
2807
 
2808
    /**
2809
     * Tests for event validations related to course module deletion.
2810
     */
2811
    public function test_course_module_deleted_event_exceptions() {
2812
 
2813
        $this->resetAfterTest();
2814
 
2815
        // Generate data.
2816
        $modinfo = $this->create_specific_module_test('assign');
2817
        $context = context_module::instance($modinfo->coursemodule);
2818
 
2819
        // Test not setting instanceid.
2820
        try {
2821
            $event = \core\event\course_module_deleted::create(array(
2822
                'courseid' => $modinfo->course,
2823
                'context'  => $context,
2824
                'objectid' => $modinfo->coursemodule,
2825
                'other'    => array(
2826
                    'modulename' => 'assign',
2827
                    'name'       => 'My assignment',
2828
                )
2829
            ));
2830
            $this->fail("Event validation should not allow \\core\\event\\course_module_deleted to be triggered without
2831
                    other['instanceid']");
2832
        } catch (coding_exception $e) {
2833
            $this->assertStringContainsString("The 'instanceid' value must be set in other.", $e->getMessage());
2834
        }
2835
 
2836
        // Test not setting modulename.
2837
        try {
2838
            $event = \core\event\course_module_deleted::create(array(
2839
                'courseid' => $modinfo->course,
2840
                'context'  => $context,
2841
                'objectid' => $modinfo->coursemodule,
2842
                'other'    => array(
2843
                    'instanceid' => $modinfo->instance,
2844
                    'name'       => 'My assignment',
2845
                )
2846
            ));
2847
            $this->fail("Event validation should not allow \\core\\event\\course_module_deleted to be triggered without
2848
                    other['modulename']");
2849
        } catch (coding_exception $e) {
2850
            $this->assertStringContainsString("The 'modulename' value must be set in other.", $e->getMessage());
2851
        }
2852
    }
2853
 
2854
    /**
2855
     * Returns a user object and its assigned new role.
2856
     *
2857
     * @param testing_data_generator $generator
2858
     * @param $contextid
2859
     * @return array The user object and the role ID
2860
     */
2861
    protected function get_user_objects(testing_data_generator $generator, $contextid) {
2862
        global $USER;
2863
 
2864
        if (empty($USER->id)) {
2865
            $user  = $generator->create_user();
2866
            $this->setUser($user);
2867
        }
2868
        $roleid = create_role('Test role', 'testrole', 'Test role description');
2869
        if (!is_array($contextid)) {
2870
            $contextid = array($contextid);
2871
        }
2872
        foreach ($contextid as $cid) {
2873
            $assignid = role_assign($roleid, $user->id, $cid);
2874
        }
2875
        return array($user, $roleid);
2876
    }
2877
 
2878
    /**
2879
     * Test course move after course.
2880
     */
2881
    public function test_course_change_sortorder_after_course() {
2882
        global $DB;
2883
 
2884
        $this->resetAfterTest(true);
2885
 
2886
        $generator = $this->getDataGenerator();
2887
        $category = $generator->create_category();
2888
        $course3 = $generator->create_course(array('category' => $category->id));
2889
        $course2 = $generator->create_course(array('category' => $category->id));
2890
        $course1 = $generator->create_course(array('category' => $category->id));
2891
        $context = $category->get_context();
2892
 
2893
        list($user, $roleid) = $this->get_user_objects($generator, $context->id);
2894
        $caps = course_capability_assignment::allow('moodle/category:manage', $roleid, $context->id);
2895
 
2896
        $courses = $category->get_courses();
2897
        $this->assertIsArray($courses);
2898
        $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2899
        $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2900
        $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2901
 
2902
        // Test moving down.
2903
        $this->assertTrue(course_change_sortorder_after_course($course1->id, $course3->id));
2904
        $courses = $category->get_courses();
2905
        $this->assertIsArray($courses);
2906
        $this->assertEquals(array($course2->id, $course3->id, $course1->id), array_keys($courses));
2907
        $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2908
        $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2909
 
2910
        // Test moving up.
2911
        $this->assertTrue(course_change_sortorder_after_course($course1->id, $course2->id));
2912
        $courses = $category->get_courses();
2913
        $this->assertIsArray($courses);
2914
        $this->assertEquals(array($course2->id, $course1->id, $course3->id), array_keys($courses));
2915
        $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2916
        $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2917
 
2918
        // Test moving to the top.
2919
        $this->assertTrue(course_change_sortorder_after_course($course1->id, 0));
2920
        $courses = $category->get_courses();
2921
        $this->assertIsArray($courses);
2922
        $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2923
        $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2924
        $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2925
    }
2926
 
2927
    /**
2928
     * Tests changing the visibility of a course.
2929
     */
2930
    public function test_course_change_visibility() {
2931
        global $DB;
2932
 
2933
        $this->resetAfterTest(true);
2934
 
2935
        $generator = $this->getDataGenerator();
2936
        $category = $generator->create_category();
2937
        $course = $generator->create_course(array('category' => $category->id));
2938
 
2939
        $this->assertEquals('1', $course->visible);
2940
        $this->assertEquals('1', $course->visibleold);
2941
 
2942
        $this->assertTrue(course_change_visibility($course->id, false));
2943
        $course = $DB->get_record('course', array('id' => $course->id));
2944
        $this->assertEquals('0', $course->visible);
2945
        $this->assertEquals('0', $course->visibleold);
2946
 
2947
        $this->assertTrue(course_change_visibility($course->id, true));
2948
        $course = $DB->get_record('course', array('id' => $course->id));
2949
        $this->assertEquals('1', $course->visible);
2950
        $this->assertEquals('1', $course->visibleold);
2951
    }
2952
 
2953
    /**
2954
     * Tests moving the course up and down by one.
2955
     */
2956
    public function test_course_change_sortorder_by_one() {
2957
        global $DB;
2958
 
2959
        $this->resetAfterTest(true);
2960
 
2961
        $generator = $this->getDataGenerator();
2962
        $category = $generator->create_category();
2963
        $course3 = $generator->create_course(array('category' => $category->id));
2964
        $course2 = $generator->create_course(array('category' => $category->id));
2965
        $course1 = $generator->create_course(array('category' => $category->id));
2966
 
2967
        $courses = $category->get_courses();
2968
        $this->assertIsArray($courses);
2969
        $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2970
        $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2971
        $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2972
 
2973
        // Test moving down.
2974
        $course1 = get_course($course1->id);
2975
        $this->assertTrue(course_change_sortorder_by_one($course1, false));
2976
        $courses = $category->get_courses();
2977
        $this->assertIsArray($courses);
2978
        $this->assertEquals(array($course2->id, $course1->id, $course3->id), array_keys($courses));
2979
        $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2980
        $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2981
 
2982
        // Test moving up.
2983
        $course1 = get_course($course1->id);
2984
        $this->assertTrue(course_change_sortorder_by_one($course1, true));
2985
        $courses = $category->get_courses();
2986
        $this->assertIsArray($courses);
2987
        $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2988
        $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2989
        $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2990
 
2991
        // Test moving the top course up one.
2992
        $course1 = get_course($course1->id);
2993
        $this->assertFalse(course_change_sortorder_by_one($course1, true));
2994
        // Check nothing changed.
2995
        $courses = $category->get_courses();
2996
        $this->assertIsArray($courses);
2997
        $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2998
        $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2999
        $this->assertEquals(array_keys($dbcourses), array_keys($courses));
3000
 
3001
        // Test moving the bottom course up down.
3002
        $course3 = get_course($course3->id);
3003
        $this->assertFalse(course_change_sortorder_by_one($course3, false));
3004
        // Check nothing changed.
3005
        $courses = $category->get_courses();
3006
        $this->assertIsArray($courses);
3007
        $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
3008
        $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
3009
        $this->assertEquals(array_keys($dbcourses), array_keys($courses));
3010
    }
3011
 
3012
    public function test_view_resources_list() {
3013
        $this->resetAfterTest();
3014
 
3015
        $course = self::getDataGenerator()->create_course();
3016
        $coursecontext = context_course::instance($course->id);
3017
 
3018
        $event = \core\event\course_resources_list_viewed::create(array('context' => context_course::instance($course->id)));
3019
        $sink = $this->redirectEvents();
3020
        $event->trigger();
3021
        $events = $sink->get_events();
3022
        $sink->close();
3023
 
3024
        // Validate the event.
3025
        $event = $events[0];
3026
        $this->assertInstanceOf('\core\event\course_resources_list_viewed', $event);
3027
        $this->assertEquals(null, $event->objecttable);
3028
        $this->assertEquals(null, $event->objectid);
3029
        $this->assertEquals($course->id, $event->courseid);
3030
        $this->assertEquals($coursecontext->id, $event->contextid);
3031
        $this->assertEventContextNotUsed($event);
3032
    }
3033
 
3034
    /**
3035
     * Test duplicate_module()
3036
     */
3037
    public function test_duplicate_module() {
3038
        $this->setAdminUser();
3039
        $this->resetAfterTest();
3040
        $course = self::getDataGenerator()->create_course();
3041
        $res = self::getDataGenerator()->create_module('resource', array('course' => $course));
3042
        $cm = get_coursemodule_from_id('resource', $res->cmid, 0, false, MUST_EXIST);
3043
 
3044
        $newcm = duplicate_module($course, $cm);
3045
 
3046
        // Make sure they are the same, except obvious id changes.
3047
        foreach ($cm as $prop => $value) {
3048
            if ($prop == 'id' || $prop == 'url' || $prop == 'instance' || $prop == 'added') {
3049
                // Ignore obviously different properties.
3050
                continue;
3051
            }
3052
            if ($prop == 'name') {
3053
                // We expect ' (copy)' to be added to the original name since MDL-59227.
3054
                $value = get_string('duplicatedmodule', 'moodle', $value);
3055
            }
3056
            $this->assertEquals($value, $newcm->$prop);
3057
        }
3058
    }
3059
 
3060
    /**
3061
     * Tests that when creating or updating a module, if the availability settings
3062
     * are present but set to an empty tree, availability is set to null in
3063
     * database.
3064
     */
3065
    public function test_empty_availability_settings() {
3066
        global $DB;
3067
        $this->setAdminUser();
3068
        $this->resetAfterTest();
3069
 
3070
        // Enable availability.
3071
        set_config('enableavailability', 1);
3072
 
3073
        // Test add.
3074
        $emptyavailability = json_encode(\core_availability\tree::get_root_json(array()));
3075
        $course = self::getDataGenerator()->create_course();
3076
        $label = self::getDataGenerator()->create_module('label', array(
3077
                'course' => $course, 'availability' => $emptyavailability));
3078
        $this->assertNull($DB->get_field('course_modules', 'availability',
3079
                array('id' => $label->cmid)));
3080
 
3081
        // Test update.
3082
        $formdata = $DB->get_record('course_modules', array('id' => $label->cmid));
3083
        unset($formdata->availability);
3084
        $formdata->availabilityconditionsjson = $emptyavailability;
3085
        $formdata->modulename = 'label';
3086
        $formdata->coursemodule = $label->cmid;
3087
        $draftid = 0;
3088
        file_prepare_draft_area($draftid, context_module::instance($label->cmid)->id,
3089
                'mod_label', 'intro', 0);
3090
        $formdata->introeditor = array(
3091
            'itemid' => $draftid,
3092
            'text' => '<p>Yo</p>',
3093
            'format' => FORMAT_HTML);
3094
        update_module($formdata);
3095
        $this->assertNull($DB->get_field('course_modules', 'availability',
3096
                array('id' => $label->cmid)));
3097
    }
3098
 
3099
    /**
3100
     * Test update_inplace_editable()
3101
     */
3102
    public function test_update_module_name_inplace() {
3103
        global $CFG, $DB, $PAGE;
3104
        require_once($CFG->dirroot . '/lib/external/externallib.php');
3105
 
3106
        $this->setUser($this->getDataGenerator()->create_user());
3107
 
3108
        $this->resetAfterTest(true);
3109
        $course = $this->getDataGenerator()->create_course();
3110
        $forum = self::getDataGenerator()->create_module('forum', array('course' => $course->id, 'name' => 'forum name'));
3111
 
3112
        // Call service for core_course component without necessary permissions.
3113
        try {
3114
            core_external::update_inplace_editable('core_course', 'activityname', $forum->cmid, 'New forum name');
3115
            $this->fail('Exception expected');
3116
        } catch (moodle_exception $e) {
3117
            $this->assertEquals('Course or activity not accessible. (Not enrolled)',
3118
                $e->getMessage());
3119
        }
3120
 
3121
        // Change to admin user and make sure that cm name can be updated using web service update_inplace_editable().
3122
        $this->setAdminUser();
3123
        $res = core_external::update_inplace_editable('core_course', 'activityname', $forum->cmid, 'New forum name');
3124
        $res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
3125
        $this->assertEquals('New forum name', $res['value']);
3126
        $this->assertEquals('New forum name', $DB->get_field('forum', 'name', array('id' => $forum->id)));
3127
    }
3128
 
3129
    /**
3130
     * Testing function course_get_tagged_course_modules - search tagged course modules
3131
     */
3132
    public function test_course_get_tagged_course_modules() {
3133
        global $DB;
3134
        $this->resetAfterTest();
3135
        $course3 = $this->getDataGenerator()->create_course();
3136
        $course2 = $this->getDataGenerator()->create_course();
3137
        $course1 = $this->getDataGenerator()->create_course();
3138
        $cm11 = $this->getDataGenerator()->create_module('assign', array('course' => $course1->id,
3139
            'tags' => 'Cat, Dog'));
3140
        $cm12 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
3141
            'tags' => 'Cat, Mouse', 'visible' => 0));
3142
        $cm13 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
3143
            'tags' => 'Cat, Mouse, Dog'));
3144
        $cm21 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id,
3145
            'tags' => 'Cat, Mouse'));
3146
        $cm31 = $this->getDataGenerator()->create_module('forum', array('course' => $course3->id,
3147
            'tags' => 'Cat, Mouse'));
3148
 
3149
        // Admin is able to view everything.
3150
        $this->setAdminUser();
3151
        $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3152
                /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$page = */0);
3153
        $this->assertMatchesRegularExpression('/'.$cm11->name.'/', $res->content);
3154
        $this->assertMatchesRegularExpression('/'.$cm12->name.'/', $res->content);
3155
        $this->assertMatchesRegularExpression('/'.$cm13->name.'/', $res->content);
3156
        $this->assertMatchesRegularExpression('/'.$cm21->name.'/', $res->content);
3157
        $this->assertMatchesRegularExpression('/'.$cm31->name.'/', $res->content);
3158
        // Results from course1 are returned before results from course2.
3159
        $this->assertTrue(strpos($res->content, $cm11->name) < strpos($res->content, $cm21->name));
3160
 
3161
        // Ordinary user is not able to see anything.
3162
        $user = $this->getDataGenerator()->create_user();
3163
        $this->setUser($user);
3164
 
3165
        $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3166
                /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$page = */0);
3167
        $this->assertNull($res);
3168
 
3169
        // Enrol user as student in course1 and course2.
3170
        $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
3171
        $this->getDataGenerator()->enrol_user($user->id, $course1->id, $roleids['student']);
3172
        $this->getDataGenerator()->enrol_user($user->id, $course2->id, $roleids['student']);
3173
        core_tag_index_builder::reset_caches();
3174
 
3175
        // Searching in the course context returns visible modules in this course.
3176
        $context = context_course::instance($course1->id);
3177
        $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3178
                /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$context->id, /*$rec = */1, /*$page = */0);
3179
        $this->assertMatchesRegularExpression('/'.$cm11->name.'/', $res->content);
3180
        $this->assertDoesNotMatchRegularExpression('/'.$cm12->name.'/', $res->content);
3181
        $this->assertMatchesRegularExpression('/'.$cm13->name.'/', $res->content);
3182
        $this->assertDoesNotMatchRegularExpression('/'.$cm21->name.'/', $res->content);
3183
        $this->assertDoesNotMatchRegularExpression('/'.$cm31->name.'/', $res->content);
3184
 
3185
        // Searching FROM the course context returns visible modules in all courses.
3186
        $context = context_course::instance($course2->id);
3187
        $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3188
                /*$exclusivemode = */false, /*$fromctx = */$context->id, /*$ctx = */0, /*$rec = */1, /*$page = */0);
3189
        $this->assertMatchesRegularExpression('/'.$cm11->name.'/', $res->content);
3190
        $this->assertDoesNotMatchRegularExpression('/'.$cm12->name.'/', $res->content);
3191
        $this->assertMatchesRegularExpression('/'.$cm13->name.'/', $res->content);
3192
        $this->assertMatchesRegularExpression('/'.$cm21->name.'/', $res->content);
3193
        $this->assertDoesNotMatchRegularExpression('/'.$cm31->name.'/', $res->content); // No access to course3.
3194
        // Results from course2 are returned before results from course1.
3195
        $this->assertTrue(strpos($res->content, $cm21->name) < strpos($res->content, $cm11->name));
3196
 
3197
        // Enrol user in course1 as a teacher - now he should be able to see hidden module.
3198
        $this->getDataGenerator()->enrol_user($user->id, $course1->id, $roleids['editingteacher']);
3199
        get_fast_modinfo(0,0,true);
3200
 
3201
        $context = context_course::instance($course1->id);
3202
        $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3203
                /*$exclusivemode = */false, /*$fromctx = */$context->id, /*$ctx = */0, /*$rec = */1, /*$page = */0);
3204
        $this->assertMatchesRegularExpression('/'.$cm12->name.'/', $res->content);
3205
 
3206
        // Create more modules and try pagination.
3207
        $cm14 = $this->getDataGenerator()->create_module('assign', array('course' => $course1->id,
3208
            'tags' => 'Cat, Dog'));
3209
        $cm15 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
3210
            'tags' => 'Cat, Mouse', 'visible' => 0));
3211
        $cm16 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
3212
            'tags' => 'Cat, Mouse, Dog'));
3213
 
3214
        $context = context_course::instance($course1->id);
3215
        $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3216
                /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$context->id, /*$rec = */1, /*$page = */0);
3217
        $this->assertMatchesRegularExpression('/'.$cm11->name.'/', $res->content);
3218
        $this->assertMatchesRegularExpression('/'.$cm12->name.'/', $res->content);
3219
        $this->assertMatchesRegularExpression('/'.$cm13->name.'/', $res->content);
3220
        $this->assertDoesNotMatchRegularExpression('/'.$cm21->name.'/', $res->content);
3221
        $this->assertMatchesRegularExpression('/'.$cm14->name.'/', $res->content);
3222
        $this->assertMatchesRegularExpression('/'.$cm15->name.'/', $res->content);
3223
        $this->assertDoesNotMatchRegularExpression('/'.$cm16->name.'/', $res->content);
3224
        $this->assertDoesNotMatchRegularExpression('/'.$cm31->name.'/', $res->content); // No access to course3.
3225
        $this->assertEmpty($res->prevpageurl);
3226
        $this->assertNotEmpty($res->nextpageurl);
3227
 
3228
        $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3229
                /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$context->id, /*$rec = */1, /*$page = */1);
3230
        $this->assertDoesNotMatchRegularExpression('/'.$cm11->name.'/', $res->content);
3231
        $this->assertDoesNotMatchRegularExpression('/'.$cm12->name.'/', $res->content);
3232
        $this->assertDoesNotMatchRegularExpression('/'.$cm13->name.'/', $res->content);
3233
        $this->assertDoesNotMatchRegularExpression('/'.$cm21->name.'/', $res->content);
3234
        $this->assertDoesNotMatchRegularExpression('/'.$cm14->name.'/', $res->content);
3235
        $this->assertDoesNotMatchRegularExpression('/'.$cm15->name.'/', $res->content);
3236
        $this->assertMatchesRegularExpression('/'.$cm16->name.'/', $res->content);
3237
        $this->assertDoesNotMatchRegularExpression('/'.$cm31->name.'/', $res->content); // No access to course3.
3238
        $this->assertNotEmpty($res->prevpageurl);
3239
        $this->assertEmpty($res->nextpageurl);
3240
    }
3241
 
3242
    /**
3243
     * Test course_get_user_navigation_options for frontpage.
3244
     */
3245
    public function test_course_get_user_navigation_options_for_frontpage() {
3246
        global $CFG, $SITE, $DB;
3247
        $this->resetAfterTest();
3248
        $context = context_system::instance();
3249
        $course = clone $SITE;
3250
        $this->setAdminUser();
3251
 
3252
        $navoptions = course_get_user_navigation_options($context, $course);
3253
        $this->assertTrue($navoptions->blogs);
3254
        $this->assertTrue($navoptions->notes);
3255
        $this->assertTrue($navoptions->participants);
3256
        $this->assertTrue($navoptions->badges);
3257
        $this->assertTrue($navoptions->tags);
3258
        $this->assertFalse($navoptions->search);
3259
        $this->assertTrue($navoptions->competencies);
3260
 
3261
        // Enable global search now.
3262
        $CFG->enableglobalsearch = 1;
3263
        $navoptions = course_get_user_navigation_options($context, $course);
3264
        $this->assertTrue($navoptions->search);
3265
 
3266
        // Disable competencies.
3267
        $oldcompetencies = get_config('core_competency', 'enabled');
3268
        set_config('enabled', false, 'core_competency');
3269
        $navoptions = course_get_user_navigation_options($context, $course);
3270
        $this->assertFalse($navoptions->competencies);
3271
        set_config('enabled', $oldcompetencies, 'core_competency');
3272
 
3273
        // Now try with a standard user.
3274
        $user = $this->getDataGenerator()->create_user();
3275
        $this->setUser($user);
3276
        $navoptions = course_get_user_navigation_options($context, $course);
3277
        $this->assertTrue($navoptions->blogs);
3278
        $this->assertFalse($navoptions->notes);
3279
        $this->assertFalse($navoptions->participants);
3280
        $this->assertTrue($navoptions->badges);
3281
        $this->assertTrue($navoptions->tags);
3282
        $this->assertTrue($navoptions->search);
3283
    }
3284
 
3285
    /**
3286
     * Test course_get_user_navigation_options for managers in a normal course.
3287
     */
3288
    public function test_course_get_user_navigation_options_for_managers() {
3289
        global $CFG;
3290
        $this->resetAfterTest();
3291
        $course = $this->getDataGenerator()->create_course();
3292
        $context = context_course::instance($course->id);
3293
        $this->setAdminUser();
3294
 
3295
        $navoptions = course_get_user_navigation_options($context);
3296
        $this->assertTrue($navoptions->blogs);
3297
        $this->assertTrue($navoptions->notes);
3298
        $this->assertTrue($navoptions->participants);
3299
        $this->assertTrue($navoptions->badges);
3300
    }
3301
 
3302
    /**
3303
     * Test course_get_user_navigation_options for students in a normal course.
3304
     */
3305
    public function test_course_get_user_navigation_options_for_students() {
3306
        global $DB, $CFG;
3307
        $this->resetAfterTest();
3308
        $course = $this->getDataGenerator()->create_course();
3309
        $context = context_course::instance($course->id);
3310
 
3311
        $user = $this->getDataGenerator()->create_user();
3312
        $roleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
3313
        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
3314
 
3315
        $this->setUser($user);
3316
 
3317
        $navoptions = course_get_user_navigation_options($context);
3318
        $this->assertTrue($navoptions->blogs);
3319
        $this->assertFalse($navoptions->notes);
3320
        $this->assertTrue($navoptions->participants);
3321
        $this->assertFalse($navoptions->badges);
3322
 
3323
        // Disable some options.
3324
        $CFG->badges_allowcoursebadges = 0;
3325
        $CFG->enableblogs = 0;
3326
        // Disable view participants capability.
3327
        assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $context);
3328
 
3329
        $navoptions = course_get_user_navigation_options($context);
3330
        $this->assertFalse($navoptions->blogs);
3331
        $this->assertFalse($navoptions->notes);
3332
        $this->assertFalse($navoptions->participants);
3333
        $this->assertFalse($navoptions->badges);
3334
 
3335
        // Re-enable some options to check badges are displayed as expected.
3336
        $CFG->badges_allowcoursebadges = 1;
3337
        assign_capability('moodle/badges:createbadge', CAP_ALLOW, $roleid, $context);
3338
 
3339
        $navoptions = course_get_user_navigation_options($context);
3340
        $this->assertTrue($navoptions->badges);
3341
    }
3342
 
3343
    /**
3344
     * Test course_get_user_administration_options for frontpage.
3345
     */
3346
    public function test_course_get_user_administration_options_for_frontpage() {
3347
        global $CFG, $SITE;
3348
        $this->resetAfterTest();
3349
        $course = clone $SITE;
3350
        $context = context_course::instance($course->id);
3351
        $this->setAdminUser();
3352
 
3353
        $adminoptions = course_get_user_administration_options($course, $context);
3354
        $this->assertTrue($adminoptions->update);
3355
        $this->assertTrue($adminoptions->filters);
3356
        $this->assertTrue($adminoptions->reports);
3357
        $this->assertTrue($adminoptions->backup);
3358
        $this->assertTrue($adminoptions->restore);
3359
        $this->assertFalse($adminoptions->files);
3360
        $this->assertFalse($adminoptions->tags);
3361
 
3362
        // Now try with a standard user.
3363
        $user = $this->getDataGenerator()->create_user();
3364
        $this->setUser($user);
3365
        $adminoptions = course_get_user_administration_options($course, $context);
3366
        $this->assertFalse($adminoptions->update);
3367
        $this->assertFalse($adminoptions->filters);
3368
        $this->assertFalse($adminoptions->reports);
3369
        $this->assertFalse($adminoptions->backup);
3370
        $this->assertFalse($adminoptions->restore);
3371
        $this->assertFalse($adminoptions->files);
3372
        $this->assertFalse($adminoptions->tags);
3373
 
3374
    }
3375
 
3376
    /**
3377
     * Test course_get_user_administration_options for managers in a normal course.
3378
     */
3379
    public function test_course_get_user_administration_options_for_managers() {
3380
        global $CFG;
3381
        $this->resetAfterTest();
3382
        $course = $this->getDataGenerator()->create_course();
3383
        $context = context_course::instance($course->id);
3384
        $this->setAdminUser();
3385
 
3386
        $adminoptions = course_get_user_administration_options($course, $context);
3387
        $this->assertTrue($adminoptions->update);
3388
        $this->assertTrue($adminoptions->filters);
3389
        $this->assertTrue($adminoptions->reports);
3390
        $this->assertTrue($adminoptions->backup);
3391
        $this->assertTrue($adminoptions->restore);
3392
        $this->assertFalse($adminoptions->files);
3393
        $this->assertTrue($adminoptions->tags);
3394
        $this->assertTrue($adminoptions->gradebook);
3395
        $this->assertFalse($adminoptions->outcomes);
3396
        $this->assertTrue($adminoptions->badges);
3397
        $this->assertTrue($adminoptions->import);
3398
        $this->assertTrue($adminoptions->reset);
3399
        $this->assertTrue($adminoptions->roles);
3400
    }
3401
 
3402
    /**
3403
     * Test course_get_user_administration_options for students in a normal course.
3404
     */
3405
    public function test_course_get_user_administration_options_for_students() {
3406
        global $DB, $CFG;
3407
        $this->resetAfterTest();
3408
        $course = $this->getDataGenerator()->create_course();
3409
        $context = context_course::instance($course->id);
3410
 
3411
        $user = $this->getDataGenerator()->create_user();
3412
        $roleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
3413
        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
3414
 
3415
        $this->setUser($user);
3416
        $adminoptions = course_get_user_administration_options($course, $context);
3417
 
3418
        $this->assertFalse($adminoptions->update);
3419
        $this->assertFalse($adminoptions->filters);
3420
        $this->assertFalse($adminoptions->reports);
3421
        $this->assertFalse($adminoptions->backup);
3422
        $this->assertFalse($adminoptions->restore);
3423
        $this->assertFalse($adminoptions->files);
3424
        $this->assertFalse($adminoptions->tags);
3425
        $this->assertFalse($adminoptions->gradebook);
3426
        $this->assertFalse($adminoptions->outcomes);
3427
        $this->assertTrue($adminoptions->badges);
3428
        $this->assertFalse($adminoptions->import);
3429
        $this->assertFalse($adminoptions->reset);
3430
        $this->assertFalse($adminoptions->roles);
3431
 
3432
        $CFG->enablebadges = false;
3433
        $adminoptions = course_get_user_administration_options($course, $context);
3434
        $this->assertFalse($adminoptions->badges);
3435
    }
3436
 
3437
    /**
3438
     * Test test_update_course_frontpage_category.
3439
     */
3440
    public function test_update_course_frontpage_category() {
3441
        // Fetch front page course.
3442
        $course = get_course(SITEID);
3443
        // Test update information on front page course.
3444
        $course->category = 99;
3445
        $this->expectException('moodle_exception');
3446
        $this->expectExceptionMessage(get_string('invalidcourse', 'error'));
3447
        update_course($course);
3448
    }
3449
 
3450
    /**
3451
     * test_course_enddate
3452
     *
3453
     * @dataProvider course_enddate_provider
3454
     * @param int $startdate
3455
     * @param int $enddate
3456
     * @param string $errorcode
3457
     */
3458
    public function test_course_enddate($startdate, $enddate, $errorcode) {
3459
 
3460
        $this->resetAfterTest(true);
3461
 
3462
        $record = array('startdate' => $startdate, 'enddate' => $enddate);
3463
        try {
3464
            $course1 = $this->getDataGenerator()->create_course($record);
3465
            if ($errorcode !== false) {
3466
                $this->fail('Expected exception with "' . $errorcode . '" error code in create_create');
3467
            }
3468
        } catch (moodle_exception $e) {
3469
            if ($errorcode === false) {
3470
                $this->fail('Got "' . $errorcode . '" exception error code and no exception was expected');
3471
            }
3472
            if ($e->errorcode != $errorcode) {
3473
                $this->fail('Got "' . $e->errorcode. '" exception error code and "' . $errorcode . '" was expected');
3474
            }
3475
            return;
3476
        }
3477
 
3478
        $this->assertEquals($startdate, $course1->startdate);
3479
        $this->assertEquals($enddate, $course1->enddate);
3480
    }
3481
 
3482
    /**
3483
     * Provider for test_course_enddate.
3484
     *
3485
     * @return array
3486
     */
3487
    public function course_enddate_provider() {
3488
        // Each provided example contains startdate, enddate and the expected exception error code if there is any.
3489
        return [
3490
            [
3491
                111,
3492
                222,
3493
                false
3494
            ], [
3495
                222,
3496
                111,
3497
                'enddatebeforestartdate'
3498
            ], [
3499
                111,
3500
                0,
3501
                false
3502
            ], [
3503
                0,
3504
                222,
3505
                'nostartdatenoenddate'
3506
            ]
3507
        ];
3508
    }
3509
 
3510
 
3511
    /**
3512
     * test_course_dates_reset
3513
     *
3514
     * @dataProvider course_dates_reset_provider
3515
     * @param int $startdate
3516
     * @param int $enddate
3517
     * @param int $resetstartdate
3518
     * @param int $resetenddate
3519
     * @param int $resultingstartdate
3520
     * @param int $resultingenddate
3521
     */
3522
    public function test_course_dates_reset($startdate, $enddate, $resetstartdate, $resetenddate, $resultingstartdate, $resultingenddate) {
3523
        global $CFG, $DB;
3524
 
3525
        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
3526
 
3527
        $this->resetAfterTest(true);
3528
 
3529
        $this->setAdminUser();
3530
 
3531
        $CFG->enablecompletion = true;
3532
 
3533
        $this->setTimezone('UTC');
3534
 
3535
        $record = array('startdate' => $startdate, 'enddate' => $enddate, 'enablecompletion' => 1);
3536
        $originalcourse = $this->getDataGenerator()->create_course($record);
3537
        $coursecriteria = new completion_criteria_date(array('course' => $originalcourse->id, 'timeend' => $startdate + DAYSECS));
3538
        $coursecriteria->insert();
3539
 
3540
        $activitycompletiondate = $startdate + DAYSECS;
3541
        $data = $this->getDataGenerator()->create_module('data', array('course' => $originalcourse->id),
3542
                        array('completion' => 1, 'completionexpected' => $activitycompletiondate));
3543
 
3544
        $resetdata = new stdClass();
3545
        $resetdata->id = $originalcourse->id;
3546
        $resetdata->reset_start_date_old = $originalcourse->startdate;
3547
        $resetdata->reset_start_date = $resetstartdate;
3548
        $resetdata->reset_end_date = $resetenddate;
3549
        $resetdata->reset_end_date_old = $record['enddate'];
3550
        reset_course_userdata($resetdata);
3551
 
3552
        $course = $DB->get_record('course', array('id' => $originalcourse->id));
3553
 
3554
        $this->assertEquals($resultingstartdate, $course->startdate);
3555
        $this->assertEquals($resultingenddate, $course->enddate);
3556
 
3557
        $coursecompletioncriteria = completion_criteria_date::fetch(array('course' => $originalcourse->id));
3558
        $this->assertEquals($resultingstartdate + DAYSECS, $coursecompletioncriteria->timeend);
3559
 
3560
        $this->assertEquals($resultingstartdate + DAYSECS, $DB->get_field('course_modules', 'completionexpected',
3561
            array('id' => $data->cmid)));
3562
    }
3563
 
3564
    /**
3565
     * Provider for test_course_dates_reset.
3566
     *
3567
     * @return array
3568
     */
3569
    public function course_dates_reset_provider() {
3570
 
3571
        // Each example contains the following:
3572
        // - course startdate
3573
        // - course enddate
3574
        // - startdate to reset to (false if not reset)
3575
        // - enddate to reset to (false if not reset)
3576
        // - resulting startdate
3577
        // - resulting enddate
3578
        $time = 1445644800;
3579
        return [
3580
            // No date changes.
3581
            [
3582
                $time,
3583
                $time + DAYSECS,
3584
                false,
3585
                false,
3586
                $time,
3587
                $time + DAYSECS
3588
            ],
3589
            // End date changes to a valid value.
3590
            [
3591
                $time,
3592
                $time + DAYSECS,
3593
                false,
3594
                $time + DAYSECS + 111,
3595
                $time,
3596
                $time + DAYSECS + 111
3597
            ],
3598
            // Start date changes to a valid value. End date does not get updated because it does not have value.
3599
            [
3600
                $time,
3601
                0,
3602
                $time + DAYSECS,
3603
                false,
3604
                $time + DAYSECS,
3605
 
3606
            ],
3607
            // Start date changes to a valid value. End date gets updated accordingly.
3608
            [
3609
                $time,
3610
                $time + DAYSECS,
3611
                $time + WEEKSECS,
3612
                false,
3613
                $time + WEEKSECS,
3614
                $time + WEEKSECS + DAYSECS
3615
            ],
3616
            // Start date and end date change to a valid value.
3617
            [
3618
                $time,
3619
                $time + DAYSECS,
3620
                $time + WEEKSECS,
3621
                $time + YEARSECS,
3622
                $time + WEEKSECS,
3623
                $time + YEARSECS
3624
            ]
3625
        ];
3626
    }
3627
 
3628
    /**
3629
     * Test reset_course_userdata()
3630
     *    - with reset_roles_overrides enabled
3631
     *    - with selective role unenrolments
3632
     */
3633
    public function test_course_roles_reset() {
3634
        global $DB;
3635
 
3636
        $this->resetAfterTest(true);
3637
 
3638
        $generator = $this->getDataGenerator();
3639
 
3640
        // Create test course and user, enrol one in the other.
3641
        $course = $generator->create_course();
3642
        $user = $generator->create_user();
3643
        $roleid = $DB->get_field('role', 'id', array('shortname' => 'student'), MUST_EXIST);
3644
        $generator->enrol_user($user->id, $course->id, $roleid);
3645
 
3646
        // Test case with reset_roles_overrides enabled.
3647
        // Override course so it does NOT allow students 'mod/forum:viewdiscussion'.
3648
        $coursecontext = context_course::instance($course->id);
3649
        assign_capability('mod/forum:viewdiscussion', CAP_PREVENT, $roleid, $coursecontext->id);
3650
 
3651
        // Check expected capabilities so far.
3652
        $this->assertFalse(has_capability('mod/forum:viewdiscussion', $coursecontext, $user));
3653
 
3654
        // Oops, preventing student from viewing forums was a mistake, let's reset the course.
3655
        $resetdata = new stdClass();
3656
        $resetdata->id = $course->id;
3657
        $resetdata->reset_roles_overrides = true;
3658
        reset_course_userdata($resetdata);
3659
 
3660
        // Check new expected capabilities - override at the course level should be reset.
3661
        $this->assertTrue(has_capability('mod/forum:viewdiscussion', $coursecontext, $user));
3662
 
3663
        // Test case with selective role unenrolments.
3664
        $roles = array();
3665
        $roles['student'] = $DB->get_field('role', 'id', array('shortname' => 'student'), MUST_EXIST);
3666
        $roles['teacher'] = $DB->get_field('role', 'id', array('shortname' => 'teacher'), MUST_EXIST);
3667
 
3668
        // We enrol a user with student and teacher roles.
3669
        $generator->enrol_user($user->id, $course->id, $roles['student']);
3670
        $generator->enrol_user($user->id, $course->id, $roles['teacher']);
3671
 
3672
        // When we reset only student role, we expect to keep teacher role.
3673
        $resetdata = new stdClass();
3674
        $resetdata->id = $course->id;
3675
        $resetdata->unenrol_users = array($roles['student']);
3676
        reset_course_userdata($resetdata);
3677
 
3678
        $usersroles = enrol_get_course_users_roles($course->id);
3679
        $this->assertArrayHasKey($user->id, $usersroles);
3680
        $this->assertArrayHasKey($roles['teacher'], $usersroles[$user->id]);
3681
        $this->assertArrayNotHasKey($roles['student'], $usersroles[$user->id]);
3682
        $this->assertCount(1, $usersroles[$user->id]);
3683
 
3684
        // We reenrol user as student.
3685
        $generator->enrol_user($user->id, $course->id, $roles['student']);
3686
 
3687
        // When we reset student and teacher roles, we expect no roles left.
3688
        $resetdata = new stdClass();
3689
        $resetdata->id = $course->id;
3690
        $resetdata->unenrol_users = array($roles['student'], $roles['teacher']);
3691
        reset_course_userdata($resetdata);
3692
 
3693
        $usersroles = enrol_get_course_users_roles($course->id);
3694
        $this->assertEmpty($usersroles);
3695
    }
3696
 
3697
    public function test_course_check_module_updates_since() {
3698
        global $CFG, $DB, $USER;
3699
        require_once($CFG->dirroot . '/mod/glossary/lib.php');
3700
        require_once($CFG->dirroot . '/rating/lib.php');
3701
        require_once($CFG->dirroot . '/comment/lib.php');
3702
 
3703
        $this->resetAfterTest(true);
3704
 
3705
        $CFG->enablecompletion = true;
3706
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
3707
        $glossary = $this->getDataGenerator()->create_module('glossary', array(
3708
            'course' => $course->id,
3709
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
3710
            'completionview' => 1,
3711
            'allowcomments' => 1,
3712
            'assessed' => RATING_AGGREGATE_AVERAGE,
3713
            'scale' => 100
3714
        ));
3715
        $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
3716
        $context = context_module::instance($glossary->cmid);
3717
        $modinfo = get_fast_modinfo($course);
3718
        $cm = $modinfo->get_cm($glossary->cmid);
3719
        $user = $this->getDataGenerator()->create_user();
3720
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
3721
        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
3722
        $from = time();
3723
 
3724
        $teacher = $this->getDataGenerator()->create_user();
3725
        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
3726
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
3727
 
3728
        assign_capability('mod/glossary:viewanyrating', CAP_ALLOW, $studentrole->id, $context->id, true);
3729
 
3730
        // Check nothing changed right now.
3731
        $updates = course_check_module_updates_since($cm, $from);
3732
        $this->assertFalse($updates->configuration->updated);
3733
        $this->assertFalse($updates->completion->updated);
3734
        $this->assertFalse($updates->gradeitems->updated);
3735
        $this->assertFalse($updates->comments->updated);
3736
        $this->assertFalse($updates->ratings->updated);
3737
        $this->assertFalse($updates->introfiles->updated);
3738
        $this->assertFalse($updates->outcomes->updated);
3739
 
3740
        $this->waitForSecond();
3741
 
3742
        // Do some changes.
3743
        $this->setUser($user);
3744
        $entry = $glossarygenerator->create_content($glossary);
3745
 
3746
        $this->setUser($teacher);
3747
        // Name.
3748
        set_coursemodule_name($glossary->cmid, 'New name');
3749
 
3750
        // Add some ratings.
3751
        $rm = new rating_manager();
3752
        $result = $rm->add_rating($cm, $context, 'mod_glossary', 'entry', $entry->id, 100, 50, $user->id, RATING_AGGREGATE_AVERAGE);
3753
 
3754
        // Change grades.
3755
        $glossary->cmidnumber = $glossary->cmid;
3756
        glossary_update_grades($glossary, $user->id);
3757
 
3758
        $this->setUser($user);
3759
        // Completion status.
3760
        glossary_view($glossary, $course, $cm, $context, 'letter');
3761
 
3762
        // Add one comment.
3763
        $args = new stdClass;
3764
        $args->context   = $context;
3765
        $args->course    = $course;
3766
        $args->cm        = $cm;
3767
        $args->area      = 'glossary_entry';
3768
        $args->itemid    = $entry->id;
3769
        $args->client_id = 1;
3770
        $args->component = 'mod_glossary';
3771
        $manager = new comment($args);
3772
        $manager->add('blah blah blah');
3773
 
3774
        // Check upgrade status.
3775
        $updates = course_check_module_updates_since($cm, $from);
3776
        $this->assertTrue($updates->configuration->updated);
3777
        $this->assertTrue($updates->completion->updated);
3778
        $this->assertTrue($updates->gradeitems->updated);
3779
        $this->assertTrue($updates->comments->updated);
3780
        $this->assertTrue($updates->ratings->updated);
3781
        $this->assertFalse($updates->introfiles->updated);
3782
        $this->assertFalse($updates->outcomes->updated);
3783
    }
3784
 
3785
    public function test_async_module_deletion_hook_implemented() {
3786
        // Async module deletion depends on the 'true' being returned by at least one plugin implementing the hook,
3787
        // 'course_module_adhoc_deletion_recommended'. In core, is implemented by the course recyclebin, which will only return
3788
        // true if the recyclebin plugin is enabled. To make sure async deletion occurs, this test force-enables the recyclebin.
3789
        global $DB, $USER;
3790
        $this->resetAfterTest(true);
3791
        $this->setAdminUser();
3792
 
3793
        // Ensure recyclebin is enabled.
3794
        set_config('coursebinenable', true, 'tool_recyclebin');
3795
 
3796
        // Create course, module and context.
3797
        $course = $this->getDataGenerator()->create_course(['numsections' => 5]);
3798
        $module = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
3799
        $modcontext = context_module::instance($module->cmid);
3800
 
3801
        // Verify context exists.
3802
        $this->assertInstanceOf('context_module', $modcontext);
3803
 
3804
        // Check events generated on the course_delete_module call.
3805
        $sink = $this->redirectEvents();
3806
 
3807
        // Try to delete the module using the async flag.
3808
        course_delete_module($module->cmid, true); // Try to delete the module asynchronously.
3809
 
3810
        // Verify that no event has been generated yet.
3811
        $events = $sink->get_events();
3812
        $event = array_pop($events);
3813
        $sink->close();
3814
        $this->assertEmpty($event);
3815
 
3816
        // Grab the record, in it's final state before hard deletion, for comparison with the event snapshot.
3817
        // We need to do this because the 'deletioninprogress' flag has changed from '0' to '1'.
3818
        $cm = $DB->get_record('course_modules', ['id' => $module->cmid], '*', MUST_EXIST);
3819
 
3820
        // Verify the course_module is marked as 'deletioninprogress'.
3821
        $this->assertNotEquals($cm, false);
3822
        $this->assertEquals($cm->deletioninprogress, '1');
3823
 
3824
        // Verify the context has not yet been removed.
3825
        $this->assertEquals($modcontext, context_module::instance($module->cmid, IGNORE_MISSING));
3826
 
3827
        // Set up a sink to catch the 'course_module_deleted' event.
3828
        $sink = $this->redirectEvents();
3829
 
3830
        // Now, run the adhoc task which performs the hard deletion.
3831
        phpunit_util::run_all_adhoc_tasks();
3832
 
3833
        // Fetch and validate the event data.
3834
        $events = $sink->get_events();
3835
        $event = array_pop($events);
3836
        $sink->close();
3837
        $this->assertInstanceOf('\core\event\course_module_deleted', $event);
3838
        $this->assertEquals($module->cmid, $event->objectid);
3839
        $this->assertEquals($USER->id, $event->userid);
3840
        $this->assertEquals('course_modules', $event->objecttable);
3841
        $this->assertEquals(null, $event->get_url());
3842
        $this->assertEquals($cm, $event->get_record_snapshot('course_modules', $module->cmid));
3843
 
3844
        // Verify the context has been removed.
3845
        $this->assertFalse(context_module::instance($module->cmid, IGNORE_MISSING));
3846
 
3847
        // Verify the course_module record has been deleted.
3848
        $cmcount = $DB->count_records('course_modules', ['id' => $module->cmid]);
3849
        $this->assertEmpty($cmcount);
3850
    }
3851
 
3852
    public function test_async_module_deletion_hook_not_implemented() {
3853
        // Only proceed if we are sure that no plugin is going to advocate async removal of a module. I.e. no plugin returns
3854
        // 'true' from the 'course_module_adhoc_deletion_recommended' hook.
3855
        // In the case of core, only recyclebin implements this hook, and it will only return true if enabled, so disable it.
3856
        global $DB, $USER;
3857
        $this->resetAfterTest(true);
3858
        $this->setAdminUser();
3859
        set_config('coursebinenable', false, 'tool_recyclebin');
3860
 
3861
        // Non-core plugins might implement the 'course_module_adhoc_deletion_recommended' hook and spoil this test.
3862
        // If at least one plugin still returns true, then skip this test.
3863
        if ($pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
3864
            foreach ($pluginsfunction as $plugintype => $plugins) {
3865
                foreach ($plugins as $pluginfunction) {
3866
                    if ($pluginfunction()) {
3867
                        $this->markTestSkipped();
3868
                    }
3869
                }
3870
            }
3871
        }
3872
 
3873
        // Create course, module and context.
3874
        $course = $this->getDataGenerator()->create_course(['numsections' => 5]);
3875
        $module = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
3876
        $modcontext = context_module::instance($module->cmid);
3877
        $cm = $DB->get_record('course_modules', ['id' => $module->cmid], '*', MUST_EXIST);
3878
 
3879
        // Verify context exists.
3880
        $this->assertInstanceOf('context_module', $modcontext);
3881
 
3882
        // Check events generated on the course_delete_module call.
3883
        $sink = $this->redirectEvents();
3884
 
3885
        // Try to delete the module using the async flag.
3886
        course_delete_module($module->cmid, true); // Try to delete the module asynchronously.
3887
 
3888
        // Fetch and validate the event data.
3889
        $events = $sink->get_events();
3890
        $event = array_pop($events);
3891
        $sink->close();
3892
        $this->assertInstanceOf('\core\event\course_module_deleted', $event);
3893
        $this->assertEquals($module->cmid, $event->objectid);
3894
        $this->assertEquals($USER->id, $event->userid);
3895
        $this->assertEquals('course_modules', $event->objecttable);
3896
        $this->assertEquals(null, $event->get_url());
3897
        $this->assertEquals($cm, $event->get_record_snapshot('course_modules', $module->cmid));
3898
 
3899
        // Verify the context has been removed.
3900
        $this->assertFalse(context_module::instance($module->cmid, IGNORE_MISSING));
3901
 
3902
        // Verify the course_module record has been deleted.
3903
        $cmcount = $DB->count_records('course_modules', ['id' => $module->cmid]);
3904
        $this->assertEmpty($cmcount);
3905
    }
3906
 
3907
    public function test_async_section_deletion_hook_implemented() {
3908
        // Async section deletion (provided section contains modules), depends on the 'true' being returned by at least one plugin
3909
        // implementing the 'course_module_adhoc_deletion_recommended' hook. In core, is implemented by the course recyclebin,
3910
        // which will only return true if the plugin is enabled. To make sure async deletion occurs, this test enables recyclebin.
3911
        global $DB, $USER;
3912
        $this->resetAfterTest(true);
3913
        $this->setAdminUser();
3914
 
3915
        // Ensure recyclebin is enabled.
3916
        set_config('coursebinenable', true, 'tool_recyclebin');
3917
 
3918
        // Create course, module and context.
3919
        $generator = $this->getDataGenerator();
3920
        $course = $generator->create_course(['numsections' => 4, 'format' => 'topics'], ['createsections' => true]);
3921
        $assign0 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
3922
        $assign1 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
3923
        $assign2 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
3924
        $assign3 = $generator->create_module('assign', ['course' => $course, 'section' => 0]);
3925
 
3926
        // Delete empty section. No difference from normal, synchronous behaviour.
3927
        $this->assertTrue(course_delete_section($course, 4, false, true));
3928
        $this->assertEquals(3, course_get_format($course)->get_last_section_number());
3929
 
3930
        // Delete a module in section 2 (using async). Need to verify this doesn't generate two tasks when we delete
3931
        // the section in the next step.
3932
        course_delete_module($assign2->cmid, true);
3933
 
3934
        // Confirm that the module is pending deletion in its current section.
3935
        $section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => '2']); // For event comparison.
3936
        $this->assertEquals(true, $DB->record_exists('course_modules', ['id' => $assign2->cmid, 'deletioninprogress' => 1,
3937
                                                     'section' => $section->id]));
3938
 
3939
        // Now, delete section 2.
3940
        $this->assertFalse(course_delete_section($course, 2, false, true)); // Non-empty section, no forcedelete, so no change.
3941
 
3942
        $sink = $this->redirectEvents(); // To capture the event.
3943
        $this->assertTrue(course_delete_section($course, 2, true, true));
3944
 
3945
        // Now, confirm that:
3946
        // a) the section's modules have been flagged for deletion and moved to section 0 and;
3947
        // b) the section has been deleted and;
3948
        // c) course_section_deleted event has been fired. The course_module_deleted events will only fire once they have been
3949
        // removed from section 0 via the adhoc task.
3950
 
3951
        // Modules should have been flagged for deletion and moved to section 0.
3952
        $sectionid = $DB->get_field('course_sections', 'id', ['course' => $course->id, 'section' => 0]);
3953
        $this->assertEquals(3, $DB->count_records('course_modules', ['section' => $sectionid, 'deletioninprogress' => 1]));
3954
 
3955
        // Confirm the section has been deleted.
3956
        $this->assertEquals(2, course_get_format($course)->get_last_section_number());
3957
 
3958
        // Check event fired.
3959
        $events = $sink->get_events();
3960
        $event = array_pop($events);
3961
        $sink->close();
3962
        $this->assertInstanceOf('\core\event\course_section_deleted', $event);
3963
        $this->assertEquals($section->id, $event->objectid);
3964
        $this->assertEquals($USER->id, $event->userid);
3965
        $this->assertEquals('course_sections', $event->objecttable);
3966
        $this->assertEquals(null, $event->get_url());
3967
        $this->assertEquals($section, $event->get_record_snapshot('course_sections', $section->id));
3968
 
3969
        // Now, run the adhoc task to delete the modules from section 0.
3970
        $sink = $this->redirectEvents(); // To capture the events.
3971
        phpunit_util::run_all_adhoc_tasks();
3972
 
3973
        // Confirm the modules have been deleted.
3974
        list($insql, $assignids) = $DB->get_in_or_equal([$assign0->cmid, $assign1->cmid, $assign2->cmid]);
3975
        $cmcount = $DB->count_records_select('course_modules', 'id ' . $insql,  $assignids);
3976
        $this->assertEmpty($cmcount);
3977
 
3978
        // Confirm other modules in section 0 still remain.
3979
        $this->assertEquals(1, $DB->count_records('course_modules', ['id' => $assign3->cmid]));
3980
 
3981
        // Confirm that events were generated for all 3 of the modules.
3982
        $events = $sink->get_events();
3983
        $sink->close();
3984
        $count = 0;
3985
        while (!empty($events)) {
3986
            $event = array_pop($events);
3987
            if ($event instanceof \core\event\course_module_deleted &&
3988
                in_array($event->objectid, [$assign0->cmid, $assign1->cmid, $assign2->cmid])) {
3989
                $count++;
3990
            }
3991
        }
3992
        $this->assertEquals(3, $count);
3993
    }
3994
 
3995
    public function test_async_section_deletion_hook_not_implemented() {
3996
        // If no plugins advocate async removal, then normal synchronous removal will take place.
3997
        // Only proceed if we are sure that no plugin is going to advocate async removal of a module. I.e. no plugin returns
3998
        // 'true' from the 'course_module_adhoc_deletion_recommended' hook.
3999
        // In the case of core, only recyclebin implements this hook, and it will only return true if enabled, so disable it.
4000
        global $DB, $USER;
4001
        $this->resetAfterTest(true);
4002
        $this->setAdminUser();
4003
        set_config('coursebinenable', false, 'tool_recyclebin');
4004
 
4005
        // Non-core plugins might implement the 'course_module_adhoc_deletion_recommended' hook and spoil this test.
4006
        // If at least one plugin still returns true, then skip this test.
4007
        if ($pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
4008
            foreach ($pluginsfunction as $plugintype => $plugins) {
4009
                foreach ($plugins as $pluginfunction) {
4010
                    if ($pluginfunction()) {
4011
                        $this->markTestSkipped();
4012
                    }
4013
                }
4014
            }
4015
        }
4016
 
4017
        // Create course, module and context.
4018
        $generator = $this->getDataGenerator();
4019
        $course = $generator->create_course(['numsections' => 4, 'format' => 'topics'], ['createsections' => true]);
4020
        $assign0 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
4021
        $assign1 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
4022
 
4023
        // Delete empty section. No difference from normal, synchronous behaviour.
4024
        $this->assertTrue(course_delete_section($course, 4, false, true));
4025
        $this->assertEquals(3, course_get_format($course)->get_last_section_number());
4026
 
4027
        // Delete section in the middle (2).
4028
        $section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => '2']); // For event comparison.
4029
        $this->assertFalse(course_delete_section($course, 2, false, true)); // Non-empty section, no forcedelete, so no change.
4030
 
4031
        $sink = $this->redirectEvents(); // To capture the event.
4032
        $this->assertTrue(course_delete_section($course, 2, true, true));
4033
 
4034
        // Now, confirm that:
4035
        // a) The section's modules have deleted and;
4036
        // b) the section has been deleted and;
4037
        // c) course_section_deleted event has been fired and;
4038
        // d) course_module_deleted events have both been fired.
4039
 
4040
        // Confirm modules have been deleted.
4041
        list($insql, $assignids) = $DB->get_in_or_equal([$assign0->cmid, $assign1->cmid]);
4042
        $cmcount = $DB->count_records_select('course_modules', 'id ' . $insql, $assignids);
4043
        $this->assertEmpty($cmcount);
4044
 
4045
        // Confirm the section has been deleted.
4046
        $this->assertEquals(2, course_get_format($course)->get_last_section_number());
4047
 
4048
        // Confirm the course_section_deleted event has been generated.
4049
        $events = $sink->get_events();
4050
        $event = array_pop($events);
4051
        $sink->close();
4052
        $this->assertInstanceOf('\core\event\course_section_deleted', $event);
4053
        $this->assertEquals($section->id, $event->objectid);
4054
        $this->assertEquals($USER->id, $event->userid);
4055
        $this->assertEquals('course_sections', $event->objecttable);
4056
        $this->assertEquals(null, $event->get_url());
4057
        $this->assertEquals($section, $event->get_record_snapshot('course_sections', $section->id));
4058
 
4059
        // Confirm that the course_module_deleted events have both been generated.
4060
        $count = 0;
4061
        while (!empty($events)) {
4062
            $event = array_pop($events);
4063
            if ($event instanceof \core\event\course_module_deleted &&
4064
                in_array($event->objectid, [$assign0->cmid, $assign1->cmid])) {
4065
                $count++;
4066
            }
4067
        }
4068
        $this->assertEquals(2, $count);
4069
    }
4070
 
4071
    public function test_classify_course_for_timeline() {
4072
        global $DB, $CFG;
4073
 
4074
        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php');
4075
 
4076
        set_config('enablecompletion', COMPLETION_ENABLED);
4077
        set_config('coursegraceperiodbefore', 0);
4078
        set_config('coursegraceperiodafter', 0);
4079
 
4080
        $this->resetAfterTest(true);
4081
        $this->setAdminUser();
4082
 
4083
        // Create courses for testing.
4084
        $generator = $this->getDataGenerator();
4085
        $future = time() + 3600;
4086
        $past = time() - 3600;
4087
        $futurecourse = $generator->create_course(['startdate' => $future]);
4088
        $pastcourse = $generator->create_course(['startdate' => $past - 60, 'enddate' => $past]);
4089
        $completedcourse = $generator->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4090
        $inprogresscourse = $generator->create_course();
4091
 
4092
        // Set completion rules.
4093
        $criteriadata = new stdClass();
4094
        $criteriadata->id = $completedcourse->id;
4095
 
4096
        // Self completion.
4097
        $criteriadata->criteria_self = COMPLETION_CRITERIA_TYPE_SELF;
4098
        $class = 'completion_criteria_self';
4099
        $criterion = new $class();
4100
        $criterion->update_config($criteriadata);
4101
 
4102
        $user = $this->getDataGenerator()->create_user();
4103
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
4104
        $this->getDataGenerator()->enrol_user($user->id, $futurecourse->id, $studentrole->id);
4105
        $this->getDataGenerator()->enrol_user($user->id, $pastcourse->id, $studentrole->id);
4106
        $this->getDataGenerator()->enrol_user($user->id, $completedcourse->id, $studentrole->id);
4107
        $this->getDataGenerator()->enrol_user($user->id, $inprogresscourse->id, $studentrole->id);
4108
 
4109
        $this->setUser($user);
4110
        core_completion_external::mark_course_self_completed($completedcourse->id);
4111
        $ccompletion = new completion_completion(array('course' => $completedcourse->id, 'userid' => $user->id));
4112
        $ccompletion->mark_complete();
4113
 
4114
        // Aggregate the completions.
4115
        $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($pastcourse));
4116
        $this->assertEquals(COURSE_TIMELINE_FUTURE, course_classify_for_timeline($futurecourse));
4117
        $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($completedcourse));
4118
        $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($inprogresscourse));
4119
 
4120
        // Test grace period.
4121
        set_config('coursegraceperiodafter', 1);
4122
        set_config('coursegraceperiodbefore', 1);
4123
        $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($pastcourse));
4124
        $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($futurecourse));
4125
        $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($completedcourse));
4126
        $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($inprogresscourse));
4127
    }
4128
 
4129
    /**
4130
     * Test the main function for updating all calendar events for a module.
4131
     */
4132
    public function test_course_module_calendar_event_update_process() {
4133
        global $DB;
4134
 
4135
        $this->resetAfterTest();
4136
        $this->setAdminUser();
4137
 
4138
        $completionexpected = time();
4139
        $duedate = time();
4140
 
4141
        $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4142
        $assign = $this->getDataGenerator()->create_module('assign', [
4143
                    'course' => $course,
4144
                    'completionexpected' => $completionexpected,
4145
                    'duedate' => $duedate
4146
                ]);
4147
 
4148
        $cm = get_coursemodule_from_instance('assign', $assign->id, $course->id);
4149
        $events = $DB->get_records('event', ['courseid' => $course->id, 'instance' => $assign->id]);
4150
        // Check that both events are using the expected dates.
4151
        foreach ($events as $event) {
4152
            if ($event->eventtype == \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED) {
4153
                $this->assertEquals($completionexpected, $event->timestart);
4154
            }
4155
            if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
4156
                $this->assertEquals($duedate, $event->timestart);
4157
            }
4158
        }
4159
 
4160
        // We have to manually update the module and the course module.
4161
        $newcompletionexpected = time() + DAYSECS * 60;
4162
        $newduedate = time() + DAYSECS * 45;
4163
        $newmodulename = 'Assign - new name';
4164
 
4165
        $moduleobject = (object)array('id' => $assign->id, 'duedate' => $newduedate, 'name' => $newmodulename);
4166
        $DB->update_record('assign', $moduleobject);
4167
        $cmobject = (object)array('id' => $cm->id, 'completionexpected' => $newcompletionexpected);
4168
        $DB->update_record('course_modules', $cmobject);
4169
 
4170
        $assign = $DB->get_record('assign', ['id' => $assign->id]);
4171
        $cm = get_coursemodule_from_instance('assign', $assign->id, $course->id);
4172
 
4173
        course_module_calendar_event_update_process($assign, $cm);
4174
 
4175
        $events = $DB->get_records('event', ['courseid' => $course->id, 'instance' => $assign->id]);
4176
        // Now check that the details have been updated properly from the function.
4177
        foreach ($events as $event) {
4178
            if ($event->eventtype == \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED) {
4179
                $this->assertEquals($newcompletionexpected, $event->timestart);
4180
                $this->assertEquals(get_string('completionexpectedfor', 'completion', (object)['instancename' => $newmodulename]),
4181
                        $event->name);
4182
            }
4183
            if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
4184
                $this->assertEquals($newduedate, $event->timestart);
4185
                $this->assertEquals(get_string('calendardue', 'assign', $newmodulename), $event->name);
4186
            }
4187
        }
4188
    }
4189
 
4190
    /**
4191
     * Test the higher level checks for updating calendar events for an instance.
4192
     */
4193
    public function test_course_module_update_calendar_events() {
4194
        $this->resetAfterTest();
4195
        $this->setAdminUser();
4196
 
4197
        $completionexpected = time();
4198
        $duedate = time();
4199
 
4200
        $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4201
        $assign = $this->getDataGenerator()->create_module('assign', [
4202
                    'course' => $course,
4203
                    'completionexpected' => $completionexpected,
4204
                    'duedate' => $duedate
4205
                ]);
4206
 
4207
        $cm = get_coursemodule_from_instance('assign', $assign->id, $course->id);
4208
 
4209
        // Both the instance and cm objects are missing.
4210
        $this->assertFalse(course_module_update_calendar_events('assign'));
4211
        // Just using the assign instance.
4212
        $this->assertTrue(course_module_update_calendar_events('assign', $assign));
4213
        // Just using the course module object.
4214
        $this->assertTrue(course_module_update_calendar_events('assign', null, $cm));
4215
        // Using both the assign instance and the course module object.
4216
        $this->assertTrue(course_module_update_calendar_events('assign', $assign, $cm));
4217
    }
4218
 
4219
    /**
4220
     * Test the higher level checks for updating calendar events for a module.
4221
     */
4222
    public function test_course_module_bulk_update_calendar_events() {
4223
        $this->resetAfterTest();
4224
        $this->setAdminUser();
4225
 
4226
        $completionexpected = time();
4227
        $duedate = time();
4228
 
4229
        $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4230
        $course2 = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4231
        $assign = $this->getDataGenerator()->create_module('assign', [
4232
                    'course' => $course,
4233
                    'completionexpected' => $completionexpected,
4234
                    'duedate' => $duedate
4235
                ]);
4236
 
4237
        // No assign instances in this course.
4238
        $this->assertFalse(course_module_bulk_update_calendar_events('assign', $course2->id));
4239
        // No book instances for the site.
4240
        $this->assertFalse(course_module_bulk_update_calendar_events('book'));
4241
        // Update all assign instances.
4242
        $this->assertTrue(course_module_bulk_update_calendar_events('assign'));
4243
        // Update the assign instances for this course.
4244
        $this->assertTrue(course_module_bulk_update_calendar_events('assign', $course->id));
4245
    }
4246
 
4247
    /**
4248
     * Test that a student can view participants in a course they are enrolled in.
4249
     */
4250
    public function test_course_can_view_participants_as_student() {
4251
        $this->resetAfterTest();
4252
 
4253
        $course = $this->getDataGenerator()->create_course();
4254
        $coursecontext = context_course::instance($course->id);
4255
 
4256
        $user = $this->getDataGenerator()->create_user();
4257
        $this->getDataGenerator()->enrol_user($user->id, $course->id);
4258
 
4259
        $this->setUser($user);
4260
 
4261
        $this->assertTrue(course_can_view_participants($coursecontext));
4262
    }
4263
 
4264
    /**
4265
     * Test that a student in a course can not view participants on the site.
4266
     */
4267
    public function test_course_can_view_participants_as_student_on_site() {
4268
        $this->resetAfterTest();
4269
 
4270
        $course = $this->getDataGenerator()->create_course();
4271
 
4272
        $user = $this->getDataGenerator()->create_user();
4273
        $this->getDataGenerator()->enrol_user($user->id, $course->id);
4274
 
4275
        $this->setUser($user);
4276
 
4277
        $this->assertFalse(course_can_view_participants(context_system::instance()));
4278
    }
4279
 
4280
    /**
4281
     * Test that an admin can view participants on the site.
4282
     */
4283
    public function test_course_can_view_participants_as_admin_on_site() {
4284
        $this->resetAfterTest();
4285
 
4286
        $this->setAdminUser();
4287
 
4288
        $this->assertTrue(course_can_view_participants(context_system::instance()));
4289
    }
4290
 
4291
    /**
4292
     * Test teachers can view participants in a course they are enrolled in.
4293
     */
4294
    public function test_course_can_view_participants_as_teacher() {
4295
        global $DB;
4296
 
4297
        $this->resetAfterTest();
4298
 
4299
        $course = $this->getDataGenerator()->create_course();
4300
        $coursecontext = context_course::instance($course->id);
4301
 
4302
        $user = $this->getDataGenerator()->create_user();
4303
        $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
4304
        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
4305
 
4306
        $this->setUser($user);
4307
 
4308
        $this->assertTrue(course_can_view_participants($coursecontext));
4309
    }
4310
 
4311
    /**
4312
     * Check the teacher can still view the participants page without the 'viewparticipants' cap.
4313
     */
4314
    public function test_course_can_view_participants_as_teacher_without_view_participants_cap() {
4315
        global $DB;
4316
 
4317
        $this->resetAfterTest();
4318
 
4319
        $course = $this->getDataGenerator()->create_course();
4320
        $coursecontext = context_course::instance($course->id);
4321
 
4322
        $user = $this->getDataGenerator()->create_user();
4323
        $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
4324
        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
4325
 
4326
        $this->setUser($user);
4327
 
4328
        // Disable one of the capabilties.
4329
        assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $coursecontext);
4330
 
4331
        // Should still be able to view the page as they have the 'moodle/course:enrolreview' cap.
4332
        $this->assertTrue(course_can_view_participants($coursecontext));
4333
    }
4334
 
4335
    /**
4336
     * Check the teacher can still view the participants page without the 'moodle/course:enrolreview' cap.
4337
     */
4338
    public function test_course_can_view_participants_as_teacher_without_enrol_review_cap() {
4339
        global $DB;
4340
 
4341
        $this->resetAfterTest();
4342
 
4343
        $course = $this->getDataGenerator()->create_course();
4344
        $coursecontext = context_course::instance($course->id);
4345
 
4346
        $user = $this->getDataGenerator()->create_user();
4347
        $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
4348
        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
4349
 
4350
        $this->setUser($user);
4351
 
4352
        // Disable one of the capabilties.
4353
        assign_capability('moodle/course:enrolreview', CAP_PROHIBIT, $roleid, $coursecontext);
4354
 
4355
        // Should still be able to view the page as they have the 'moodle/course:viewparticipants' cap.
4356
        $this->assertTrue(course_can_view_participants($coursecontext));
4357
    }
4358
 
4359
    /**
4360
     * Check the teacher can not view the participants page without the required caps.
4361
     */
4362
    public function test_course_can_view_participants_as_teacher_without_required_caps() {
4363
        global $DB;
4364
 
4365
        $this->resetAfterTest();
4366
 
4367
        $course = $this->getDataGenerator()->create_course();
4368
        $coursecontext = context_course::instance($course->id);
4369
 
4370
        $user = $this->getDataGenerator()->create_user();
4371
        $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
4372
        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
4373
 
4374
        $this->setUser($user);
4375
 
4376
        // Disable the capabilities.
4377
        assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $coursecontext);
4378
        assign_capability('moodle/course:enrolreview', CAP_PROHIBIT, $roleid, $coursecontext);
4379
 
4380
        $this->assertFalse(course_can_view_participants($coursecontext));
4381
    }
4382
 
4383
    /**
4384
     * Check that an exception is not thrown if we can view the participants page.
4385
     */
4386
    public function test_course_require_view_participants() {
4387
        $this->resetAfterTest();
4388
 
4389
        $course = $this->getDataGenerator()->create_course();
4390
        $coursecontext = context_course::instance($course->id);
4391
 
4392
        $user = $this->getDataGenerator()->create_user();
4393
        $this->getDataGenerator()->enrol_user($user->id, $course->id);
4394
 
4395
        $this->setUser($user);
4396
 
4397
        course_require_view_participants($coursecontext);
4398
    }
4399
 
4400
    /**
4401
     * Check that an exception is thrown if we can't view the participants page.
4402
     */
4403
    public function test_course_require_view_participants_as_student_on_site() {
4404
        $this->resetAfterTest();
4405
 
4406
        $course = $this->getDataGenerator()->create_course();
4407
 
4408
        $user = $this->getDataGenerator()->create_user();
4409
        $this->getDataGenerator()->enrol_user($user->id, $course->id);
4410
 
4411
        $this->setUser($user);
4412
 
4413
        $this->expectException('required_capability_exception');
4414
        course_require_view_participants(context_system::instance());
4415
    }
4416
 
4417
    /**
4418
     *  Testing the can_download_from_backup_filearea fn.
4419
     */
4420
    public function test_can_download_from_backup_filearea() {
4421
        global $DB;
4422
        $this->resetAfterTest();
4423
        $course = $this->getDataGenerator()->create_course();
4424
        $context = context_course::instance($course->id);
4425
        $user = $this->getDataGenerator()->create_user();
4426
        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
4427
        $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id);
4428
 
4429
        // The 'automated' backup area. Downloading from this area requires two capabilities.
4430
        // If the user has only the 'backup:downloadfile' capability.
4431
        unassign_capability('moodle/restore:userinfo', $teacherrole->id, $context);
4432
        assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
4433
        $this->assertFalse(can_download_from_backup_filearea('automated', $context, $user));
4434
 
4435
        // If the user has only the 'restore:userinfo' capability.
4436
        unassign_capability('moodle/backup:downloadfile', $teacherrole->id, $context);
4437
        assign_capability('moodle/restore:userinfo', CAP_ALLOW, $teacherrole->id, $context);
4438
        $this->assertFalse(can_download_from_backup_filearea('automated', $context, $user));
4439
 
4440
        // If the user has both capabilities.
4441
        assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
4442
        assign_capability('moodle/restore:userinfo', CAP_ALLOW, $teacherrole->id, $context);
4443
        $this->assertTrue(can_download_from_backup_filearea('automated', $context, $user));
4444
 
4445
        // Is the user has neither of the capabilities.
4446
        unassign_capability('moodle/backup:downloadfile', $teacherrole->id, $context);
4447
        unassign_capability('moodle/restore:userinfo', $teacherrole->id, $context);
4448
        $this->assertFalse(can_download_from_backup_filearea('automated', $context, $user));
4449
 
4450
        // The 'course ' and 'backup' backup file areas. These are governed by the same download capability.
4451
        // User has the capability.
4452
        unassign_capability('moodle/restore:userinfo', $teacherrole->id, $context);
4453
        assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
4454
        $this->assertTrue(can_download_from_backup_filearea('course', $context, $user));
4455
        $this->assertTrue(can_download_from_backup_filearea('backup', $context, $user));
4456
 
4457
        // User doesn't have the capability.
4458
        unassign_capability('moodle/backup:downloadfile', $teacherrole->id, $context);
4459
        $this->assertFalse(can_download_from_backup_filearea('course', $context, $user));
4460
        $this->assertFalse(can_download_from_backup_filearea('backup', $context, $user));
4461
 
4462
        // A file area that doesn't exist. No permissions, regardless of capabilities.
4463
        assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
4464
        $this->assertFalse(can_download_from_backup_filearea('testing', $context, $user));
4465
    }
4466
 
4467
    /**
4468
     * Test cases for the course_classify_courses_for_timeline test.
4469
     */
4470
    public function get_course_classify_courses_for_timeline_test_cases() {
4471
        $now = time();
4472
        $day = 86400;
4473
 
4474
        return [
4475
            'no courses' => [
4476
                'coursesdata' => [],
4477
                'expected' => [
4478
                    COURSE_TIMELINE_PAST => [],
4479
                    COURSE_TIMELINE_FUTURE => [],
4480
                    COURSE_TIMELINE_INPROGRESS => []
4481
                ]
4482
            ],
4483
            'only past' => [
4484
                'coursesdata' => [
4485
                    [
4486
                        'shortname' => 'past1',
4487
                        'startdate' => $now - ($day * 2),
4488
                        'enddate' => $now - $day
4489
                    ],
4490
                    [
4491
                        'shortname' => 'past2',
4492
                        'startdate' => $now - ($day * 2),
4493
                        'enddate' => $now - $day
4494
                    ]
4495
                ],
4496
                'expected' => [
4497
                    COURSE_TIMELINE_PAST => ['past1', 'past2'],
4498
                    COURSE_TIMELINE_FUTURE => [],
4499
                    COURSE_TIMELINE_INPROGRESS => []
4500
                ]
4501
            ],
4502
            'only in progress' => [
4503
                'coursesdata' => [
4504
                    [
4505
                        'shortname' => 'inprogress1',
4506
                        'startdate' => $now - $day,
4507
                        'enddate' => $now + $day
4508
                    ],
4509
                    [
4510
                        'shortname' => 'inprogress2',
4511
                        'startdate' => $now - $day,
4512
                        'enddate' => $now + $day
4513
                    ]
4514
                ],
4515
                'expected' => [
4516
                    COURSE_TIMELINE_PAST => [],
4517
                    COURSE_TIMELINE_FUTURE => [],
4518
                    COURSE_TIMELINE_INPROGRESS => ['inprogress1', 'inprogress2']
4519
                ]
4520
            ],
4521
            'only future' => [
4522
                'coursesdata' => [
4523
                    [
4524
                        'shortname' => 'future1',
4525
                        'startdate' => $now + $day
4526
                    ],
4527
                    [
4528
                        'shortname' => 'future2',
4529
                        'startdate' => $now + $day
4530
                    ]
4531
                ],
4532
                'expected' => [
4533
                    COURSE_TIMELINE_PAST => [],
4534
                    COURSE_TIMELINE_FUTURE => ['future1', 'future2'],
4535
                    COURSE_TIMELINE_INPROGRESS => []
4536
                ]
4537
            ],
4538
            'combination' => [
4539
                'coursesdata' => [
4540
                    [
4541
                        'shortname' => 'past1',
4542
                        'startdate' => $now - ($day * 2),
4543
                        'enddate' => $now - $day
4544
                    ],
4545
                    [
4546
                        'shortname' => 'past2',
4547
                        'startdate' => $now - ($day * 2),
4548
                        'enddate' => $now - $day
4549
                    ],
4550
                    [
4551
                        'shortname' => 'inprogress1',
4552
                        'startdate' => $now - $day,
4553
                        'enddate' => $now + $day
4554
                    ],
4555
                    [
4556
                        'shortname' => 'inprogress2',
4557
                        'startdate' => $now - $day,
4558
                        'enddate' => $now + $day
4559
                    ],
4560
                    [
4561
                        'shortname' => 'future1',
4562
                        'startdate' => $now + $day
4563
                    ],
4564
                    [
4565
                        'shortname' => 'future2',
4566
                        'startdate' => $now + $day
4567
                    ]
4568
                ],
4569
                'expected' => [
4570
                    COURSE_TIMELINE_PAST => ['past1', 'past2'],
4571
                    COURSE_TIMELINE_FUTURE => ['future1', 'future2'],
4572
                    COURSE_TIMELINE_INPROGRESS => ['inprogress1', 'inprogress2']
4573
                ]
4574
            ],
4575
        ];
4576
    }
4577
 
4578
    /**
4579
     * Test the course_classify_courses_for_timeline function.
4580
     *
4581
     * @dataProvider get_course_classify_courses_for_timeline_test_cases()
4582
     * @param array $coursesdata Courses to create
4583
     * @param array $expected Expected test results.
4584
     */
4585
    public function test_course_classify_courses_for_timeline($coursesdata, $expected) {
4586
        $this->resetAfterTest();
4587
        $generator = $this->getDataGenerator();
4588
 
4589
        $courses = array_map(function($coursedata) use ($generator) {
4590
            return $generator->create_course($coursedata);
4591
        }, $coursesdata);
4592
 
4593
        sort($expected[COURSE_TIMELINE_PAST]);
4594
        sort($expected[COURSE_TIMELINE_FUTURE]);
4595
        sort($expected[COURSE_TIMELINE_INPROGRESS]);
4596
 
4597
        $results = course_classify_courses_for_timeline($courses);
4598
 
4599
        $actualpast = array_map(function($result) {
4600
            return $result->shortname;
4601
        }, $results[COURSE_TIMELINE_PAST]);
4602
 
4603
        $actualfuture = array_map(function($result) {
4604
            return $result->shortname;
4605
        }, $results[COURSE_TIMELINE_FUTURE]);
4606
 
4607
        $actualinprogress = array_map(function($result) {
4608
            return $result->shortname;
4609
        }, $results[COURSE_TIMELINE_INPROGRESS]);
4610
 
4611
        sort($actualpast);
4612
        sort($actualfuture);
4613
        sort($actualinprogress);
4614
 
4615
        $this->assertEquals($expected[COURSE_TIMELINE_PAST], $actualpast);
4616
        $this->assertEquals($expected[COURSE_TIMELINE_FUTURE], $actualfuture);
4617
        $this->assertEquals($expected[COURSE_TIMELINE_INPROGRESS], $actualinprogress);
4618
    }
4619
 
4620
    /**
4621
     * Test cases for the course_get_enrolled_courses_for_logged_in_user tests.
4622
     */
4623
    public function get_course_get_enrolled_courses_for_logged_in_user_test_cases() {
4624
        $buildexpectedresult = function($limit, $offset) {
4625
            $result = [];
4626
            for ($i = $offset; $i < $offset + $limit; $i++) {
4627
                $result[] = "testcourse{$i}";
4628
            }
4629
            return $result;
4630
        };
4631
 
4632
        return [
4633
            'zero records' => [
4634
                'dbquerylimit' => 3,
4635
                'totalcourses' => 0,
4636
                'limit' => 0,
4637
                'offset' => 0,
4638
                'expecteddbqueries' => 4,
4639
                'expectedresult' => $buildexpectedresult(0, 0)
4640
            ],
4641
            'less than query limit' => [
4642
                'dbquerylimit' => 3,
4643
                'totalcourses' => 2,
4644
                'limit' => 0,
4645
                'offset' => 0,
4646
                'expecteddbqueries' => 2,
4647
                'expectedresult' => $buildexpectedresult(2, 0)
4648
            ],
4649
            'more than query limit' => [
4650
                'dbquerylimit' => 3,
4651
                'totalcourses' => 7,
4652
                'limit' => 0,
4653
                'offset' => 0,
4654
                'expecteddbqueries' => 4,
4655
                'expectedresult' => $buildexpectedresult(7, 0)
4656
            ],
4657
            'limit less than query limit' => [
4658
                'dbquerylimit' => 3,
4659
                'totalcourses' => 7,
4660
                'limit' => 2,
4661
                'offset' => 0,
4662
                'expecteddbqueries' => 2,
4663
                'expectedresult' => $buildexpectedresult(2, 0)
4664
            ],
4665
            'limit less than query limit with offset' => [
4666
                'dbquerylimit' => 3,
4667
                'totalcourses' => 7,
4668
                'limit' => 2,
4669
                'offset' => 2,
4670
                'expecteddbqueries' => 2,
4671
                'expectedresult' => $buildexpectedresult(2, 2)
4672
            ],
4673
            'limit less than total' => [
4674
                'dbquerylimit' => 3,
4675
                'totalcourses' => 9,
4676
                'limit' => 6,
4677
                'offset' => 0,
4678
                'expecteddbqueries' => 3,
4679
                'expectedresult' => $buildexpectedresult(6, 0)
4680
            ],
4681
            'less results than limit' => [
4682
                'dbquerylimit' => 4,
4683
                'totalcourses' => 9,
4684
                'limit' => 20,
4685
                'offset' => 0,
4686
                'expecteddbqueries' => 4,
4687
                'expectedresult' => $buildexpectedresult(9, 0)
4688
            ],
4689
            'less results than limit exact divisible' => [
4690
                'dbquerylimit' => 3,
4691
                'totalcourses' => 9,
4692
                'limit' => 20,
4693
                'offset' => 0,
4694
                'expecteddbqueries' => 5,
4695
                'expectedresult' => $buildexpectedresult(9, 0)
4696
            ],
4697
            'less results than limit with offset' => [
4698
                'dbquerylimit' => 3,
4699
                'totalcourses' => 9,
4700
                'limit' => 10,
4701
                'offset' => 5,
4702
                'expecteddbqueries' => 3,
4703
                'expectedresult' => $buildexpectedresult(4, 5)
4704
            ],
4705
        ];
4706
    }
4707
 
4708
    /**
4709
     * Test the course_get_enrolled_courses_for_logged_in_user function.
4710
     *
4711
     * @dataProvider get_course_get_enrolled_courses_for_logged_in_user_test_cases()
4712
     * @param int $dbquerylimit Number of records to load per DB request
4713
     * @param int $totalcourses Number of courses to create
4714
     * @param int $limit Maximum number of results to get.
4715
     * @param int $offset Skip this number of results from the start of the result set.
4716
     * @param int $expecteddbqueries The number of DB queries expected during the test.
4717
     * @param array $expectedresult Expected test results.
4718
     */
4719
    public function test_course_get_enrolled_courses_for_logged_in_user(
4720
        $dbquerylimit,
4721
        $totalcourses,
4722
        $limit,
4723
        $offset,
4724
        $expecteddbqueries,
4725
        $expectedresult
4726
    ) {
4727
        global $DB;
4728
 
4729
        $this->resetAfterTest();
4730
        $generator = $this->getDataGenerator();
4731
        $student = $generator->create_user();
4732
 
4733
        for ($i = 0; $i < $totalcourses; $i++) {
4734
            $shortname = "testcourse{$i}";
4735
            $course = $generator->create_course(['shortname' => $shortname]);
4736
            $generator->enrol_user($student->id, $course->id, 'student');
4737
        }
4738
 
4739
        $this->setUser($student);
4740
 
4741
        $initialquerycount = $DB->perf_get_queries();
4742
        $courses = course_get_enrolled_courses_for_logged_in_user($limit, $offset, 'shortname ASC', 'shortname', $dbquerylimit);
4743
 
4744
        // Loop over the result set to force the lazy loading to kick in so that we can check the
4745
        // number of DB queries.
4746
        $actualresult = array_map(function($course) {
4747
            return $course->shortname;
4748
        }, iterator_to_array($courses, false));
4749
 
4750
        sort($expectedresult);
4751
 
4752
        $this->assertEquals($expectedresult, $actualresult);
4753
        $this->assertLessThanOrEqual($expecteddbqueries, $DB->perf_get_queries() - $initialquerycount);
4754
    }
4755
 
4756
    /**
4757
     * Test cases for the course_filter_courses_by_timeline_classification tests.
4758
     */
4759
    public function get_course_filter_courses_by_timeline_classification_test_cases() {
4760
        $now = time();
4761
        $day = 86400;
4762
 
4763
        $coursedata = [
4764
            [
4765
                'shortname' => 'apast',
4766
                'startdate' => $now - ($day * 2),
4767
                'enddate' => $now - $day
4768
            ],
4769
            [
4770
                'shortname' => 'bpast',
4771
                'startdate' => $now - ($day * 2),
4772
                'enddate' => $now - $day
4773
            ],
4774
            [
4775
                'shortname' => 'cpast',
4776
                'startdate' => $now - ($day * 2),
4777
                'enddate' => $now - $day
4778
            ],
4779
            [
4780
                'shortname' => 'dpast',
4781
                'startdate' => $now - ($day * 2),
4782
                'enddate' => $now - $day
4783
            ],
4784
            [
4785
                'shortname' => 'epast',
4786
                'startdate' => $now - ($day * 2),
4787
                'enddate' => $now - $day
4788
            ],
4789
            [
4790
                'shortname' => 'ainprogress',
4791
                'startdate' => $now - $day,
4792
                'enddate' => $now + $day
4793
            ],
4794
            [
4795
                'shortname' => 'binprogress',
4796
                'startdate' => $now - $day,
4797
                'enddate' => $now + $day
4798
            ],
4799
            [
4800
                'shortname' => 'cinprogress',
4801
                'startdate' => $now - $day,
4802
                'enddate' => $now + $day
4803
            ],
4804
            [
4805
                'shortname' => 'dinprogress',
4806
                'startdate' => $now - $day,
4807
                'enddate' => $now + $day
4808
            ],
4809
            [
4810
                'shortname' => 'einprogress',
4811
                'startdate' => $now - $day,
4812
                'enddate' => $now + $day
4813
            ],
4814
            [
4815
                'shortname' => 'afuture',
4816
                'startdate' => $now + $day
4817
            ],
4818
            [
4819
                'shortname' => 'bfuture',
4820
                'startdate' => $now + $day
4821
            ],
4822
            [
4823
                'shortname' => 'cfuture',
4824
                'startdate' => $now + $day
4825
            ],
4826
            [
4827
                'shortname' => 'dfuture',
4828
                'startdate' => $now + $day
4829
            ],
4830
            [
4831
                'shortname' => 'efuture',
4832
                'startdate' => $now + $day
4833
            ]
4834
        ];
4835
 
4836
        // Raw enrolled courses result set should be returned in this order:
4837
        // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
4838
        // dfuture, dinprogress, dpast, efuture, einprogress, epast
4839
        //
4840
        // By classification the offset values for each record should be:
4841
        // COURSE_TIMELINE_FUTURE
4842
        // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
4843
        // COURSE_TIMELINE_INPROGRESS
4844
        // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
4845
        // COURSE_TIMELINE_PAST
4846
        // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
4847
        return [
4848
            'empty set' => [
4849
                'coursedata' => [],
4850
                'classification' => COURSE_TIMELINE_FUTURE,
4851
                'limit' => 2,
4852
                'offset' => 0,
4853
                'expectedcourses' => [],
4854
                'expectedprocessedcount' => 0
4855
            ],
4856
            // COURSE_TIMELINE_FUTURE.
4857
            'future not limit no offset' => [
4858
                'coursedata' => $coursedata,
4859
                'classification' => COURSE_TIMELINE_FUTURE,
4860
                'limit' => 0,
4861
                'offset' => 0,
4862
                'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
4863
                'expectedprocessedcount' => 15
4864
            ],
4865
            'future no offset' => [
4866
                'coursedata' => $coursedata,
4867
                'classification' => COURSE_TIMELINE_FUTURE,
4868
                'limit' => 2,
4869
                'offset' => 0,
4870
                'expectedcourses' => ['afuture', 'bfuture'],
4871
                'expectedprocessedcount' => 4
4872
            ],
4873
            'future offset' => [
4874
                'coursedata' => $coursedata,
4875
                'classification' => COURSE_TIMELINE_FUTURE,
4876
                'limit' => 2,
4877
                'offset' => 2,
4878
                'expectedcourses' => ['bfuture', 'cfuture'],
4879
                'expectedprocessedcount' => 5
4880
            ],
4881
            'future exact limit' => [
4882
                'coursedata' => $coursedata,
4883
                'classification' => COURSE_TIMELINE_FUTURE,
4884
                'limit' => 5,
4885
                'offset' => 0,
4886
                'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
4887
                'expectedprocessedcount' => 13
4888
            ],
4889
            'future limit less results' => [
4890
                'coursedata' => $coursedata,
4891
                'classification' => COURSE_TIMELINE_FUTURE,
4892
                'limit' => 10,
4893
                'offset' => 0,
4894
                'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
4895
                'expectedprocessedcount' => 15
4896
            ],
4897
            'future limit less results with offset' => [
4898
                'coursedata' => $coursedata,
4899
                'classification' => COURSE_TIMELINE_FUTURE,
4900
                'limit' => 10,
4901
                'offset' => 5,
4902
                'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
4903
                'expectedprocessedcount' => 10
4904
            ],
4905
        ];
4906
    }
4907
 
4908
    /**
4909
     * Test the course_get_enrolled_courses_for_logged_in_user_from_search function.
4910
     */
4911
    public function test_course_get_enrolled_courses_for_logged_in_user_from_search() {
4912
        global $DB;
4913
 
4914
        // Set up.
4915
 
4916
        $this->resetAfterTest();
4917
        $generator = $this->getDataGenerator();
4918
        $student = $generator->create_user();
4919
 
4920
        $cat1 = core_course_category::create(['name' => 'Cat1']);
4921
        $cat2 = core_course_category::create(['name' => 'Cat2', 'parent' => $cat1->id]);
4922
        $c1 = $this->getDataGenerator()->create_course(['category' => $cat1->id, 'fullname' => 'Test 3', 'summary' => 'Magic', 'idnumber' => 'ID3']);
4923
        $c2 = $this->getDataGenerator()->create_course(['category' => $cat1->id, 'fullname' => 'Test 1', 'summary' => 'Magic']);
4924
        $c3 = $this->getDataGenerator()->create_course(['category' => $cat1->id, 'fullname' => 'Математика', 'summary' => ' Test Magic']);
4925
        $c4 = $this->getDataGenerator()->create_course(['category' => $cat1->id, 'fullname' => 'Test 4', 'summary' => 'Magic', 'idnumber' => 'ID4']);
4926
 
4927
        $c5 = $this->getDataGenerator()->create_course(['category' => $cat2->id, 'fullname' => 'Test 5', 'summary' => 'Magic']);
4928
        $c6 = $this->getDataGenerator()->create_course(['category' => $cat2->id, 'fullname' => 'Дискретная Математика', 'summary' => 'Magic']);
4929
        $c7 = $this->getDataGenerator()->create_course(['category' => $cat2->id, 'fullname' => 'Test 7', 'summary' => 'Magic']);
4930
        $c8 = $this->getDataGenerator()->create_course(['category' => $cat2->id, 'fullname' => 'Test 8', 'summary' => 'Magic']);
4931
 
4932
        for ($i = 1; $i < 9; $i++) {
4933
            $generator->enrol_user($student->id, ${"c$i"}->id, 'student');
4934
        }
4935
 
4936
        $this->setUser($student);
4937
 
4938
        $returnedcourses = course_get_enrolled_courses_for_logged_in_user_from_search(
4939
            0,
4940
            0,
4941
            'id ASC',
4942
            null,
4943
            COURSE_DB_QUERY_LIMIT,
4944
            ['search' => 'test'],
4945
            ['idonly' => true]
4946
        );
4947
 
4948
        $actualresult = array_map(function($course) {
4949
            return $course->id;
4950
        }, iterator_to_array($returnedcourses, false));
4951
 
4952
        $this->assertEquals([$c1->id, $c2->id, $c3->id, $c4->id, $c5->id, $c7->id, $c8->id], $actualresult);
4953
 
4954
        // Test no courses matching the search.
4955
        $returnedcourses = course_get_enrolled_courses_for_logged_in_user_from_search(
4956
            0,
4957
            0,
4958
            'id ASC',
4959
            null,
4960
            COURSE_DB_QUERY_LIMIT,
4961
            ['search' => 'foobar'],
4962
            ['idonly' => true]
4963
        );
4964
 
4965
        $actualresult = array_map(function($course) {
4966
            return $course->id;
4967
        }, iterator_to_array($returnedcourses, false));
4968
 
4969
        $this->assertEquals([], $actualresult);
4970
 
4971
        // Test returning all courses that have a mutual summary.
4972
        $returnedcourses = course_get_enrolled_courses_for_logged_in_user_from_search(
4973
            0,
4974
            0,
4975
            'id ASC',
4976
            null,
4977
            COURSE_DB_QUERY_LIMIT,
4978
            ['search' => 'Magic'],
4979
            ['idonly' => true]
4980
        );
4981
 
4982
        $actualresult = array_map(function($course) {
4983
            return $course->id;
4984
        }, iterator_to_array($returnedcourses, false));
4985
 
4986
        $this->assertEquals([$c1->id, $c2->id, $c3->id, $c4->id, $c5->id, $c6->id, $c7->id, $c8->id], $actualresult);
4987
 
4988
        // Test returning a unique course.
4989
        $returnedcourses = course_get_enrolled_courses_for_logged_in_user_from_search(
4990
            0,
4991
            0,
4992
            'id ASC',
4993
            null,
4994
            COURSE_DB_QUERY_LIMIT,
4995
            ['search' => 'Дискретная'],
4996
            ['idonly' => true]
4997
        );
4998
 
4999
        $actualresult = array_map(function($course) {
5000
            return $course->id;
5001
        }, iterator_to_array($returnedcourses, false));
5002
 
5003
        $this->assertEquals([$c6->id], $actualresult);
5004
    }
5005
 
5006
    /**
5007
     * Test the course_filter_courses_by_timeline_classification function.
5008
     *
5009
     * @dataProvider get_course_filter_courses_by_timeline_classification_test_cases()
5010
     * @param array $coursedata Course test data to create.
5011
     * @param string $classification Timeline classification.
5012
     * @param int $limit Maximum number of results to return.
5013
     * @param int $offset Results to skip at the start of the result set.
5014
     * @param string[] $expectedcourses Expected courses in results.
5015
     * @param int $expectedprocessedcount Expected number of course records to be processed.
5016
     */
5017
    public function test_course_filter_courses_by_timeline_classification(
5018
        $coursedata,
5019
        $classification,
5020
        $limit,
5021
        $offset,
5022
        $expectedcourses,
5023
        $expectedprocessedcount
5024
    ) {
5025
        $this->resetAfterTest();
5026
        $generator = $this->getDataGenerator();
5027
 
5028
        $courses = array_map(function($coursedata) use ($generator) {
5029
            return $generator->create_course($coursedata);
5030
        }, $coursedata);
5031
 
5032
        $student = $generator->create_user();
5033
 
5034
        foreach ($courses as $course) {
5035
            $generator->enrol_user($student->id, $course->id, 'student');
5036
        }
5037
 
5038
        $this->setUser($student);
5039
 
5040
        $coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
5041
        list($result, $processedcount) = course_filter_courses_by_timeline_classification(
5042
            $coursesgenerator,
5043
            $classification,
5044
            $limit
5045
        );
5046
 
5047
        $actual = array_map(function($course) {
5048
            return $course->shortname;
5049
        }, $result);
5050
 
5051
        $this->assertEquals($expectedcourses, $actual);
5052
        $this->assertEquals($expectedprocessedcount, $processedcount);
5053
    }
5054
 
5055
    /**
5056
     * Test cases for the course_filter_courses_by_timeline_classification tests.
5057
     */
5058
    public function get_course_filter_courses_by_customfield_test_cases() {
5059
        global $CFG;
5060
        require_once($CFG->dirroot.'/blocks/myoverview/lib.php');
5061
        $coursedata = [
5062
            [
5063
                'shortname' => 'C1',
5064
                'customfield_checkboxfield' => 1,
5065
                'customfield_datefield' => strtotime('2001-02-01T12:00:00Z'),
5066
                'customfield_selectfield' => 1,
5067
                'customfield_textfield' => 'fish',
5068
            ],
5069
            [
5070
                'shortname' => 'C2',
5071
                'customfield_checkboxfield' => 0,
5072
                'customfield_datefield' => strtotime('1980-08-05T13:00:00Z'),
5073
            ],
5074
            [
5075
                'shortname' => 'C3',
5076
                'customfield_checkboxfield' => 0,
5077
                'customfield_datefield' => strtotime('2001-02-01T12:00:00Z'),
5078
                'customfield_selectfield' => 2,
5079
                'customfield_textfield' => 'dog',
5080
            ],
5081
            [
5082
                'shortname' => 'C4',
5083
                'customfield_checkboxfield' => 1,
5084
                'customfield_selectfield' => 3,
5085
                'customfield_textfield' => 'cat',
5086
            ],
5087
            [
5088
                'shortname' => 'C5',
5089
                'customfield_datefield' => strtotime('1980-08-06T13:00:00Z'),
5090
                'customfield_selectfield' => 2,
5091
                'customfield_textfield' => 'fish',
5092
            ],
5093
        ];
5094
 
5095
        return [
5096
            'empty set' => [
5097
                'coursedata' => [],
5098
                'customfield' => 'checkboxfield',
5099
                'customfieldvalue' => 1,
5100
                'limit' => 10,
5101
                'offset' => 0,
5102
                'expectedcourses' => [],
5103
                'expectedprocessedcount' => 0
5104
            ],
5105
            'checkbox yes' => [
5106
                'coursedata' => $coursedata,
5107
                'customfield' => 'checkboxfield',
5108
                'customfieldvalue' => 1,
5109
                'limit' => 10,
5110
                'offset' => 0,
5111
                'expectedcourses' => ['C1', 'C4'],
5112
                'expectedprocessedcount' => 5
5113
            ],
5114
            'checkbox no' => [
5115
                'coursedata' => $coursedata,
5116
                'customfield' => 'checkboxfield',
5117
                'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5118
                'limit' => 10,
5119
                'offset' => 0,
5120
                'expectedcourses' => ['C2', 'C3', 'C5'],
5121
                'expectedprocessedcount' => 5
5122
            ],
5123
            'date 1 Feb 2001' => [
5124
                'coursedata' => $coursedata,
5125
                'customfield' => 'datefield',
5126
                'customfieldvalue' => strtotime('2001-02-01T12:00:00Z'),
5127
                'limit' => 10,
5128
                'offset' => 0,
5129
                'expectedcourses' => ['C1', 'C3'],
5130
                'expectedprocessedcount' => 5
5131
            ],
5132
            'date 6 Aug 1980' => [
5133
                'coursedata' => $coursedata,
5134
                'customfield' => 'datefield',
5135
                'customfieldvalue' => strtotime('1980-08-06T13:00:00Z'),
5136
                'limit' => 10,
5137
                'offset' => 0,
5138
                'expectedcourses' => ['C5'],
5139
                'expectedprocessedcount' => 5
5140
            ],
5141
            'date no date' => [
5142
                'coursedata' => $coursedata,
5143
                'customfield' => 'datefield',
5144
                'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5145
                'limit' => 10,
5146
                'offset' => 0,
5147
                'expectedcourses' => ['C4'],
5148
                'expectedprocessedcount' => 5
5149
            ],
5150
            'select Option 1' => [
5151
                'coursedata' => $coursedata,
5152
                'customfield' => 'selectfield',
5153
                'customfieldvalue' => 1,
5154
                'limit' => 10,
5155
                'offset' => 0,
5156
                'expectedcourses' => ['C1'],
5157
                'expectedprocessedcount' => 5
5158
            ],
5159
            'select Option 2' => [
5160
                'coursedata' => $coursedata,
5161
                'customfield' => 'selectfield',
5162
                'customfieldvalue' => 2,
5163
                'limit' => 10,
5164
                'offset' => 0,
5165
                'expectedcourses' => ['C3', 'C5'],
5166
                'expectedprocessedcount' => 5
5167
            ],
5168
            'select no select' => [
5169
                'coursedata' => $coursedata,
5170
                'customfield' => 'selectfield',
5171
                'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5172
                'limit' => 10,
5173
                'offset' => 0,
5174
                'expectedcourses' => ['C2'],
5175
                'expectedprocessedcount' => 5
5176
            ],
5177
            'text fish' => [
5178
                'coursedata' => $coursedata,
5179
                'customfield' => 'textfield',
5180
                'customfieldvalue' => 'fish',
5181
                'limit' => 10,
5182
                'offset' => 0,
5183
                'expectedcourses' => ['C1', 'C5'],
5184
                'expectedprocessedcount' => 5
5185
            ],
5186
            'text dog' => [
5187
                'coursedata' => $coursedata,
5188
                'customfield' => 'textfield',
5189
                'customfieldvalue' => 'dog',
5190
                'limit' => 10,
5191
                'offset' => 0,
5192
                'expectedcourses' => ['C3'],
5193
                'expectedprocessedcount' => 5
5194
            ],
5195
            'text no text' => [
5196
                'coursedata' => $coursedata,
5197
                'customfield' => 'textfield',
5198
                'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5199
                'limit' => 10,
5200
                'offset' => 0,
5201
                'expectedcourses' => ['C2'],
5202
                'expectedprocessedcount' => 5
5203
            ],
5204
            'checkbox limit no' => [
5205
                'coursedata' => $coursedata,
5206
                'customfield' => 'checkboxfield',
5207
                'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5208
                'limit' => 2,
5209
                'offset' => 0,
5210
                'expectedcourses' => ['C2', 'C3'],
5211
                'expectedprocessedcount' => 3
5212
            ],
5213
            'checkbox limit offset no' => [
5214
                'coursedata' => $coursedata,
5215
                'customfield' => 'checkboxfield',
5216
                'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5217
                'limit' => 2,
5218
                'offset' => 3,
5219
                'expectedcourses' => ['C5'],
5220
                'expectedprocessedcount' => 2
5221
            ],
5222
        ];
5223
    }
5224
 
5225
    /**
5226
     * Test the course_filter_courses_by_customfield function.
5227
     *
5228
     * @dataProvider get_course_filter_courses_by_customfield_test_cases()
5229
     * @param array $coursedata Course test data to create.
5230
     * @param string $customfield Shortname of the customfield.
5231
     * @param string $customfieldvalue the value to filter by.
5232
     * @param int $limit Maximum number of results to return.
5233
     * @param int $offset Results to skip at the start of the result set.
5234
     * @param string[] $expectedcourses Expected courses in results.
5235
     * @param int $expectedprocessedcount Expected number of course records to be processed.
5236
     */
5237
    public function test_course_filter_courses_by_customfield(
5238
        $coursedata,
5239
        $customfield,
5240
        $customfieldvalue,
5241
        $limit,
5242
        $offset,
5243
        $expectedcourses,
5244
        $expectedprocessedcount
5245
    ) {
5246
        $this->resetAfterTest();
5247
        $generator = $this->getDataGenerator();
5248
 
5249
        // Create the custom fields.
5250
        $generator->create_custom_field_category([
5251
            'name' => 'Course fields',
5252
            'component' => 'core_course',
5253
            'area' => 'course',
5254
            'itemid' => 0,
5255
        ]);
5256
        $generator->create_custom_field([
5257
            'name' => 'Checkbox field',
5258
            'category' => 'Course fields',
5259
            'type' => 'checkbox',
5260
            'shortname' => 'checkboxfield',
5261
        ]);
5262
        $generator->create_custom_field([
5263
            'name' => 'Date field',
5264
            'category' => 'Course fields',
5265
            'type' => 'date',
5266
            'shortname' => 'datefield',
5267
            'configdata' => '{"mindate":0, "maxdate":0}',
5268
        ]);
5269
        $generator->create_custom_field([
5270
            'name' => 'Select field',
5271
            'category' => 'Course fields',
5272
            'type' => 'select',
5273
            'shortname' => 'selectfield',
5274
            'configdata' => '{"options":"Option 1\nOption 2\nOption 3\nOption 4"}',
5275
        ]);
5276
        $generator->create_custom_field([
5277
            'name' => 'Text field',
5278
            'category' => 'Course fields',
5279
            'type' => 'text',
5280
            'shortname' => 'textfield',
5281
        ]);
5282
 
5283
        $courses = array_map(function($coursedata) use ($generator) {
5284
            return $generator->create_course($coursedata);
5285
        }, $coursedata);
5286
 
5287
        $student = $generator->create_user();
5288
 
5289
        foreach ($courses as $course) {
5290
            $generator->enrol_user($student->id, $course->id, 'student');
5291
        }
5292
 
5293
        $this->setUser($student);
5294
 
5295
        $coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
5296
        list($result, $processedcount) = course_filter_courses_by_customfield(
5297
            $coursesgenerator,
5298
            $customfield,
5299
            $customfieldvalue,
5300
            $limit
5301
        );
5302
 
5303
        $actual = array_map(function($course) {
5304
            return $course->shortname;
5305
        }, $result);
5306
 
5307
        $this->assertEquals($expectedcourses, $actual);
5308
        $this->assertEquals($expectedprocessedcount, $processedcount);
5309
    }
5310
 
5311
    /**
5312
     * Test cases for the course_filter_courses_by_timeline_classification w/ hidden courses tests.
5313
     */
5314
    public function get_course_filter_courses_by_timeline_classification_hidden_courses_test_cases() {
5315
        $now = time();
5316
        $day = 86400;
5317
 
5318
        $coursedata = [
5319
            [
5320
                'shortname' => 'apast',
5321
                'startdate' => $now - ($day * 2),
5322
                'enddate' => $now - $day
5323
            ],
5324
            [
5325
                'shortname' => 'bpast',
5326
                'startdate' => $now - ($day * 2),
5327
                'enddate' => $now - $day
5328
            ],
5329
            [
5330
                'shortname' => 'cpast',
5331
                'startdate' => $now - ($day * 2),
5332
                'enddate' => $now - $day
5333
            ],
5334
            [
5335
                'shortname' => 'dpast',
5336
                'startdate' => $now - ($day * 2),
5337
                'enddate' => $now - $day
5338
            ],
5339
            [
5340
                'shortname' => 'epast',
5341
                'startdate' => $now - ($day * 2),
5342
                'enddate' => $now - $day
5343
            ],
5344
            [
5345
                'shortname' => 'ainprogress',
5346
                'startdate' => $now - $day,
5347
                'enddate' => $now + $day
5348
            ],
5349
            [
5350
                'shortname' => 'binprogress',
5351
                'startdate' => $now - $day,
5352
                'enddate' => $now + $day
5353
            ],
5354
            [
5355
                'shortname' => 'cinprogress',
5356
                'startdate' => $now - $day,
5357
                'enddate' => $now + $day
5358
            ],
5359
            [
5360
                'shortname' => 'dinprogress',
5361
                'startdate' => $now - $day,
5362
                'enddate' => $now + $day
5363
            ],
5364
            [
5365
                'shortname' => 'einprogress',
5366
                'startdate' => $now - $day,
5367
                'enddate' => $now + $day
5368
            ],
5369
            [
5370
                'shortname' => 'afuture',
5371
                'startdate' => $now + $day
5372
            ],
5373
            [
5374
                'shortname' => 'bfuture',
5375
                'startdate' => $now + $day
5376
            ],
5377
            [
5378
                'shortname' => 'cfuture',
5379
                'startdate' => $now + $day
5380
            ],
5381
            [
5382
                'shortname' => 'dfuture',
5383
                'startdate' => $now + $day
5384
            ],
5385
            [
5386
                'shortname' => 'efuture',
5387
                'startdate' => $now + $day
5388
            ]
5389
        ];
5390
 
5391
        // Raw enrolled courses result set should be returned in this order:
5392
        // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
5393
        // dfuture, dinprogress, dpast, efuture, einprogress, epast
5394
        //
5395
        // By classification the offset values for each record should be:
5396
        // COURSE_TIMELINE_FUTURE
5397
        // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
5398
        // COURSE_TIMELINE_INPROGRESS
5399
        // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
5400
        // COURSE_TIMELINE_PAST
5401
        // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
5402
        return [
5403
            'empty set' => [
5404
                'coursedata' => [],
5405
                'classification' => COURSE_TIMELINE_FUTURE,
5406
                'limit' => 2,
5407
                'offset' => 0,
5408
                'expectedcourses' => [],
5409
                'expectedprocessedcount' => 0,
5410
                'hiddencourse' => ''
5411
            ],
5412
            // COURSE_TIMELINE_FUTURE.
5413
            'future not limit no offset' => [
5414
                'coursedata' => $coursedata,
5415
                'classification' => COURSE_TIMELINE_FUTURE,
5416
                'limit' => 0,
5417
                'offset' => 0,
5418
                'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
5419
                'expectedprocessedcount' => 15,
5420
                'hiddencourse' => 'bfuture'
5421
            ],
5422
            'future no offset' => [
5423
                'coursedata' => $coursedata,
5424
                'classification' => COURSE_TIMELINE_FUTURE,
5425
                'limit' => 2,
5426
                'offset' => 0,
5427
                'expectedcourses' => ['afuture', 'cfuture'],
5428
                'expectedprocessedcount' => 7,
5429
                'hiddencourse' => 'bfuture'
5430
            ],
5431
            'future offset' => [
5432
                'coursedata' => $coursedata,
5433
                'classification' => COURSE_TIMELINE_FUTURE,
5434
                'limit' => 2,
5435
                'offset' => 2,
5436
                'expectedcourses' => ['bfuture', 'dfuture'],
5437
                'expectedprocessedcount' => 8,
5438
                'hiddencourse' => 'cfuture'
5439
            ],
5440
            'future exact limit' => [
5441
                'coursedata' => $coursedata,
5442
                'classification' => COURSE_TIMELINE_FUTURE,
5443
                'limit' => 5,
5444
                'offset' => 0,
5445
                'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
5446
                'expectedprocessedcount' => 15,
5447
                'hiddencourse' => 'bfuture'
5448
            ],
5449
            'future limit less results' => [
5450
                'coursedata' => $coursedata,
5451
                'classification' => COURSE_TIMELINE_FUTURE,
5452
                'limit' => 10,
5453
                'offset' => 0,
5454
                'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
5455
                'expectedprocessedcount' => 15,
5456
                'hiddencourse' => 'bfuture'
5457
            ],
5458
            'future limit less results with offset' => [
5459
                'coursedata' => $coursedata,
5460
                'classification' => COURSE_TIMELINE_FUTURE,
5461
                'limit' => 10,
5462
                'offset' => 5,
5463
                'expectedcourses' => ['cfuture', 'efuture'],
5464
                'expectedprocessedcount' => 10,
5465
                'hiddencourse' => 'dfuture'
5466
            ],
5467
        ];
5468
    }
5469
 
5470
    /**
5471
     * Test the course_filter_courses_by_timeline_classification function hidden courses.
5472
     *
5473
     * @dataProvider get_course_filter_courses_by_timeline_classification_hidden_courses_test_cases()
5474
     * @param array $coursedata Course test data to create.
5475
     * @param string $classification Timeline classification.
5476
     * @param int $limit Maximum number of results to return.
5477
     * @param int $offset Results to skip at the start of the result set.
5478
     * @param string[] $expectedcourses Expected courses in results.
5479
     * @param int $expectedprocessedcount Expected number of course records to be processed.
5480
     * @param int $hiddencourse The course to hide as part of this process
5481
     */
5482
    public function test_course_filter_courses_by_timeline_classification_with_hidden_courses(
5483
        $coursedata,
5484
        $classification,
5485
        $limit,
5486
        $offset,
5487
        $expectedcourses,
5488
        $expectedprocessedcount,
5489
        $hiddencourse
5490
    ) {
5491
        $this->resetAfterTest();
5492
        $generator = $this->getDataGenerator();
5493
        $student = $generator->create_user();
5494
        $this->setUser($student);
5495
 
5496
        $courses = array_map(function($coursedata) use ($generator, $hiddencourse) {
5497
            $course = $generator->create_course($coursedata);
5498
            if ($course->shortname == $hiddencourse) {
5499
                set_user_preference('block_myoverview_hidden_course_' . $course->id, true);
5500
            }
5501
            return $course;
5502
        }, $coursedata);
5503
 
5504
        foreach ($courses as $course) {
5505
            $generator->enrol_user($student->id, $course->id, 'student');
5506
        }
5507
 
5508
        $coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
5509
        list($result, $processedcount) = course_filter_courses_by_timeline_classification(
5510
            $coursesgenerator,
5511
            $classification,
5512
            $limit
5513
        );
5514
 
5515
        $actual = array_map(function($course) {
5516
            return $course->shortname;
5517
        }, $result);
5518
 
5519
        $this->assertEquals($expectedcourses, $actual);
5520
        $this->assertEquals($expectedprocessedcount, $processedcount);
5521
    }
5522
 
5523
 
5524
    /**
5525
     * Testing core_course_core_calendar_get_valid_event_timestart_range when the course has no end date.
5526
     */
5527
    public function test_core_course_core_calendar_get_valid_event_timestart_range_no_enddate() {
5528
        global $CFG;
5529
        require_once($CFG->dirroot . "/calendar/lib.php");
5530
 
5531
        $this->resetAfterTest(true);
5532
        $this->setAdminUser();
5533
        $generator = $this->getDataGenerator();
5534
        $now = time();
5535
        $course = $generator->create_course(['startdate' => $now - 86400]);
5536
 
5537
        // Create a course event.
5538
        $event = new \calendar_event([
5539
            'name' => 'Test course event',
5540
            'eventtype' => 'course',
5541
            'courseid' => $course->id,
5542
        ]);
5543
 
5544
        list ($min, $max) = core_course_core_calendar_get_valid_event_timestart_range($event, $course);
5545
        $this->assertEquals($course->startdate, $min[0]);
5546
        $this->assertNull($max);
5547
    }
5548
 
5549
    /**
5550
     * Testing core_course_core_calendar_get_valid_event_timestart_range when the course has end date.
5551
     */
5552
    public function test_core_course_core_calendar_get_valid_event_timestart_range_with_enddate() {
5553
        global $CFG;
5554
        require_once($CFG->dirroot . "/calendar/lib.php");
5555
 
5556
        $this->resetAfterTest(true);
5557
        $this->setAdminUser();
5558
        $generator = $this->getDataGenerator();
5559
        $now = time();
5560
        $course = $generator->create_course(['startdate' => $now - 86400, 'enddate' => $now + 86400]);
5561
 
5562
        // Create a course event.
5563
        $event = new \calendar_event([
5564
            'name' => 'Test course event',
5565
            'eventtype' => 'course',
5566
            'courseid' => $course->id,
5567
        ]);
5568
 
5569
        list ($min, $max) = core_course_core_calendar_get_valid_event_timestart_range($event, $course);
5570
        $this->assertEquals($course->startdate, $min[0]);
5571
        $this->assertNull($max);
5572
    }
5573
 
5574
    /**
5575
     * Test the course_get_recent_courses function.
5576
     */
5577
    public function test_course_get_recent_courses() {
5578
        global $DB;
5579
 
5580
        $this->resetAfterTest();
5581
        $generator = $this->getDataGenerator();
5582
 
5583
        $courses = array();
5584
        for ($i = 1; $i < 4; $i++) {
5585
            $courses[]  = $generator->create_course();
5586
        };
5587
 
5588
        $student = $generator->create_user();
5589
 
5590
        foreach ($courses as $course) {
5591
            $generator->enrol_user($student->id, $course->id, 'student');
5592
        }
5593
 
5594
        $this->setUser($student);
5595
 
5596
        $result = course_get_recent_courses($student->id);
5597
 
5598
        // No course accessed.
5599
        $this->assertCount(0, $result);
5600
 
5601
        $time = time();
5602
        foreach ($courses as $course) {
5603
            $context = context_course::instance($course->id);
5604
            course_view($context);
5605
            $DB->set_field('user_lastaccess', 'timeaccess', $time, [
5606
                'userid' => $student->id,
5607
                'courseid' => $course->id,
5608
                ]);
5609
            $time++;
5610
        }
5611
 
5612
        // Every course accessed.
5613
        $result = course_get_recent_courses($student->id);
5614
        $this->assertCount(3, $result);
5615
 
5616
        // Every course accessed, result limited to 2 courses.
5617
        $result = course_get_recent_courses($student->id, 2);
5618
        $this->assertCount(2, $result);
5619
 
5620
        // Every course accessed, with limit and offset should return the first course.
5621
        $result = course_get_recent_courses($student->id, 3, 2);
5622
        $this->assertCount(1, $result);
5623
        $this->assertArrayHasKey($courses[0]->id, $result);
5624
 
5625
        // Every course accessed, order by shortname DESC. The last create course ($course[2]) should have the greater shortname.
5626
        $result = course_get_recent_courses($student->id, 0, 0, 'shortname DESC');
5627
        $this->assertCount(3, $result);
5628
        $this->assertEquals($courses[2]->id, array_values($result)[0]->id);
5629
        $this->assertEquals($courses[1]->id, array_values($result)[1]->id);
5630
        $this->assertEquals($courses[0]->id, array_values($result)[2]->id);
5631
 
5632
        // Every course accessed, order by shortname ASC.
5633
        $result = course_get_recent_courses($student->id, 0, 0, 'shortname ASC');
5634
        $this->assertCount(3, $result);
5635
        $this->assertEquals($courses[0]->id, array_values($result)[0]->id);
5636
        $this->assertEquals($courses[1]->id, array_values($result)[1]->id);
5637
        $this->assertEquals($courses[2]->id, array_values($result)[2]->id);
5638
 
5639
        $guestcourse = $generator->create_course(
5640
            (object)array('shortname' => 'guestcourse',
5641
                'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
5642
                'enrol_guest_password_0' => ''));
5643
        $context = context_course::instance($guestcourse->id);
5644
        course_view($context);
5645
 
5646
        // Every course accessed, even the not enrolled one.
5647
        $result = course_get_recent_courses($student->id);
5648
        $this->assertCount(4, $result);
5649
 
5650
        // Suspended student.
5651
        $this->getDataGenerator()->enrol_user($student->id, $courses[0]->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
5652
 
5653
        // The course with suspended enrolment is not returned by the function.
5654
        $result = course_get_recent_courses($student->id);
5655
        $this->assertCount(3, $result);
5656
        $this->assertArrayNotHasKey($courses[0]->id, $result);
5657
    }
5658
 
5659
    /**
5660
     * Test the validation of the sort value in course_get_recent_courses().
5661
     *
5662
     * @dataProvider course_get_recent_courses_sort_validation_provider
5663
     * @param string $sort The sort value
5664
     * @param string $expectedexceptionmsg The expected exception message
5665
     */
5666
    public function test_course_get_recent_courses_sort_validation(string $sort, string $expectedexceptionmsg) {
5667
        $this->resetAfterTest();
5668
 
5669
        $user = $this->getDataGenerator()->create_user();
5670
 
5671
        if (!empty($expectedexceptionmsg)) {
5672
            $this->expectException('invalid_parameter_exception');
5673
            $this->expectExceptionMessage($expectedexceptionmsg);
5674
        }
5675
        course_get_recent_courses($user->id, 0, 0, $sort);
5676
    }
5677
 
5678
    /**
5679
     * Data provider for test_course_get_recent_courses_sort_validation().
5680
     *
5681
     * @return array
5682
     */
5683
    function course_get_recent_courses_sort_validation_provider() {
5684
        return [
5685
            'Invalid sort format (SQL injection attempt)' =>
5686
                [
5687
                    'shortname DESC LIMIT 1--',
5688
                    'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].',
5689
                ],
5690
            'Sort uses \'sort by\' field that does not exist' =>
5691
                [
5692
                    'shortname DESC, xyz ASC',
5693
                    'Invalid field in the sort parameter, allowed fields: id, idnumber, summary, summaryformat, ' .
5694
                    'startdate, enddate, category, shortname, fullname, timeaccess, component, visible, ' .
5695
                    'showactivitydates, showcompletionconditions, pdfexportfont.',
5696
            ],
5697
            'Sort uses invalid value for the sorting direction' =>
5698
                [
5699
                    'shortname xyz, lastaccess',
5700
                    'Invalid sort direction in the sort parameter, allowed values: asc, desc.',
5701
                ],
5702
            'Valid sort format' =>
5703
                [
5704
                    'shortname asc, timeaccess',
5705
                    ''
5706
                ]
5707
        ];
5708
    }
5709
 
5710
    /**
5711
     * Test the course_get_recent_courses function.
5712
     */
5713
    public function test_course_get_recent_courses_with_guest() {
5714
        global $DB;
5715
        $this->resetAfterTest(true);
5716
 
5717
        $student = $this->getDataGenerator()->create_user();
5718
 
5719
        // Course 1 with guest access and no direct enrolment.
5720
        $course1 = $this->getDataGenerator()->create_course();
5721
        $context1 = context_course::instance($course1->id);
5722
        $record = $DB->get_record('enrol', ['courseid' => $course1->id, 'enrol' => 'guest']);
5723
        enrol_get_plugin('guest')->update_status($record, ENROL_INSTANCE_ENABLED);
5724
 
5725
        // Course 2 where student is enrolled with two enrolment methods.
5726
        $course2 = $this->getDataGenerator()->create_course();
5727
        $context2 = context_course::instance($course2->id);
5728
        $record = $DB->get_record('enrol', ['courseid' => $course2->id, 'enrol' => 'self']);
5729
        enrol_get_plugin('guest')->update_status($record, ENROL_INSTANCE_ENABLED);
5730
        $this->getDataGenerator()->enrol_user($student->id, $course2->id, 'student', 'manual', 0, 0, ENROL_USER_ACTIVE);
5731
        $this->getDataGenerator()->enrol_user($student->id, $course2->id, 'student', 'self', 0, 0, ENROL_USER_ACTIVE);
5732
 
5733
        // Course 3.
5734
        $course3 = $this->getDataGenerator()->create_course();
5735
        $context3 = context_course::instance($course3->id);
5736
 
5737
        // Student visits first two courses, course_get_recent_courses returns two courses.
5738
        $this->setUser($student);
5739
        course_view($context1);
5740
        course_view($context2);
5741
 
5742
        $result = course_get_recent_courses($student->id);
5743
        $this->assertEqualsCanonicalizing([$course2->id, $course1->id], array_column($result, 'id'));
5744
 
5745
        // Admin visits all three courses. Only the one with guest access is returned.
5746
        $this->setAdminUser();
5747
        course_view($context1);
5748
        course_view($context2);
5749
        course_view($context3);
5750
        $result = course_get_recent_courses(get_admin()->id);
5751
        $this->assertEqualsCanonicalizing([$course1->id], array_column($result, 'id'));
5752
    }
5753
 
5754
    /**
5755
     * Test cases for the course_get_course_dates_for_user_ids tests.
5756
     */
5757
    public function get_course_get_course_dates_for_user_ids_test_cases() {
5758
        $now = time();
5759
        $pastcoursestart = $now - 100;
5760
        $futurecoursestart = $now + 100;
5761
 
5762
        return [
5763
            'future course start fixed no users enrolled' => [
5764
                'relativedatemode' => false,
5765
                'coursestart' => $futurecoursestart,
5766
                'usercount' => 2,
5767
                'enrolmentmethods' => [
5768
                    ['manual', ENROL_INSTANCE_ENABLED],
5769
                    ['self', ENROL_INSTANCE_ENABLED]
5770
                ],
5771
                'enrolled' => [[], []],
5772
                'expected' => [
5773
                    [
5774
                        'start' => $futurecoursestart,
5775
                        'startoffset' => 0
5776
                    ],
5777
                    [
5778
                        'start' => $futurecoursestart,
5779
                        'startoffset' => 0
5780
                    ]
5781
                ]
5782
            ],
5783
            'future course start fixed 1 users enrolled future' => [
5784
                'relativedatemode' => false,
5785
                'coursestart' => $futurecoursestart,
5786
                'usercount' => 2,
5787
                'enrolmentmethods' => [
5788
                    ['manual', ENROL_INSTANCE_ENABLED],
5789
                    ['self', ENROL_INSTANCE_ENABLED]
5790
                ],
5791
                'enrolled' => [
5792
                    // User 1.
5793
                    ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
5794
                    // User 2.
5795
                    []
5796
                ],
5797
                'expected' => [
5798
                    [
5799
                        'start' => $futurecoursestart,
5800
                        'startoffset' => 0
5801
                    ],
5802
                    [
5803
                        'start' => $futurecoursestart,
5804
                        'startoffset' => 0
5805
                    ]
5806
                ]
5807
            ],
5808
            'future course start fixed 1 users enrolled past' => [
5809
                'relativedatemode' => false,
5810
                'coursestart' => $futurecoursestart,
5811
                'usercount' => 2,
5812
                'enrolmentmethods' => [
5813
                    ['manual', ENROL_INSTANCE_ENABLED],
5814
                    ['self', ENROL_INSTANCE_ENABLED]
5815
                ],
5816
                'enrolled' => [
5817
                    // User 1.
5818
                    ['manual' => [$futurecoursestart - 10, ENROL_USER_ACTIVE]],
5819
                    // User 2.
5820
                    []
5821
                ],
5822
                'expected' => [
5823
                    [
5824
                        'start' => $futurecoursestart,
5825
                        'startoffset' => 0
5826
                    ],
5827
                    [
5828
                        'start' => $futurecoursestart,
5829
                        'startoffset' => 0
5830
                    ]
5831
                ]
5832
            ],
5833
            'future course start fixed 2 users enrolled future' => [
5834
                'relativedatemode' => false,
5835
                'coursestart' => $futurecoursestart,
5836
                'usercount' => 2,
5837
                'enrolmentmethods' => [
5838
                    ['manual', ENROL_INSTANCE_ENABLED],
5839
                    ['self', ENROL_INSTANCE_ENABLED]
5840
                ],
5841
                'enrolled' => [
5842
                    // User 1.
5843
                    ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
5844
                    // User 2.
5845
                    ['manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]]
5846
                ],
5847
                'expected' => [
5848
                    [
5849
                        'start' => $futurecoursestart,
5850
                        'startoffset' => 0
5851
                    ],
5852
                    [
5853
                        'start' => $futurecoursestart,
5854
                        'startoffset' => 0
5855
                    ]
5856
                ]
5857
            ],
5858
            'future course start fixed 2 users enrolled past' => [
5859
                'relativedatemode' => false,
5860
                'coursestart' => $futurecoursestart,
5861
                'usercount' => 2,
5862
                'enrolmentmethods' => [
5863
                    ['manual', ENROL_INSTANCE_ENABLED],
5864
                    ['self', ENROL_INSTANCE_ENABLED]
5865
                ],
5866
                'enrolled' => [
5867
                    // User 1.
5868
                    ['manual' => [$futurecoursestart - 10, ENROL_USER_ACTIVE]],
5869
                    // User 2.
5870
                    ['manual' => [$futurecoursestart - 20, ENROL_USER_ACTIVE]]
5871
                ],
5872
                'expected' => [
5873
                    [
5874
                        'start' => $futurecoursestart,
5875
                        'startoffset' => 0
5876
                    ],
5877
                    [
5878
                        'start' => $futurecoursestart,
5879
                        'startoffset' => 0
5880
                    ]
5881
                ]
5882
            ],
5883
            'future course start fixed 2 users enrolled mixed' => [
5884
                'relativedatemode' => false,
5885
                'coursestart' => $futurecoursestart,
5886
                'usercount' => 2,
5887
                'enrolmentmethods' => [
5888
                    ['manual', ENROL_INSTANCE_ENABLED],
5889
                    ['self', ENROL_INSTANCE_ENABLED]
5890
                ],
5891
                'enrolled' => [
5892
                    // User 1.
5893
                    ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
5894
                    // User 2.
5895
                    ['manual' => [$futurecoursestart - 20, ENROL_USER_ACTIVE]]
5896
                ],
5897
                'expected' => [
5898
                    [
5899
                        'start' => $futurecoursestart,
5900
                        'startoffset' => 0
5901
                    ],
5902
                    [
5903
                        'start' => $futurecoursestart,
5904
                        'startoffset' => 0
5905
                    ]
5906
                ]
5907
            ],
5908
            'future course start fixed 2 users enrolled 2 methods' => [
5909
                'relativedatemode' => false,
5910
                'coursestart' => $futurecoursestart,
5911
                'usercount' => 2,
5912
                'enrolmentmethods' => [
5913
                    ['manual', ENROL_INSTANCE_ENABLED],
5914
                    ['self', ENROL_INSTANCE_ENABLED]
5915
                ],
5916
                'enrolled' => [
5917
                    // User 1.
5918
                    [
5919
                        'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
5920
                        'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
5921
                    ],
5922
                    // User 2.
5923
                    [
5924
                        'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
5925
                        'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
5926
                    ]
5927
                ],
5928
                'expected' => [
5929
                    [
5930
                        'start' => $futurecoursestart,
5931
                        'startoffset' => 0
5932
                    ],
5933
                    [
5934
                        'start' => $futurecoursestart,
5935
                        'startoffset' => 0
5936
                    ]
5937
                ]
5938
            ],
5939
            'future course start fixed 2 users enrolled 2 methods 1 disabled' => [
5940
                'relativedatemode' => false,
5941
                'coursestart' => $futurecoursestart,
5942
                'usercount' => 2,
5943
                'enrolmentmethods' => [
5944
                    ['manual', ENROL_INSTANCE_DISABLED],
5945
                    ['self', ENROL_INSTANCE_ENABLED]
5946
                ],
5947
                'enrolled' => [
5948
                    // User 1.
5949
                    [
5950
                        'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
5951
                        'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
5952
                    ],
5953
                    // User 2.
5954
                    [
5955
                        'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
5956
                        'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
5957
                    ]
5958
                ],
5959
                'expected' => [
5960
                    [
5961
                        'start' => $futurecoursestart,
5962
                        'startoffset' => 0
5963
                    ],
5964
                    [
5965
                        'start' => $futurecoursestart,
5966
                        'startoffset' => 0
5967
                    ]
5968
                ]
5969
            ],
5970
            'future course start fixed 2 users enrolled 2 methods 2 disabled' => [
5971
                'relativedatemode' => false,
5972
                'coursestart' => $futurecoursestart,
5973
                'usercount' => 2,
5974
                'enrolmentmethods' => [
5975
                    ['manual', ENROL_INSTANCE_DISABLED],
5976
                    ['self', ENROL_INSTANCE_DISABLED]
5977
                ],
5978
                'enrolled' => [
5979
                    // User 1.
5980
                    [
5981
                        'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
5982
                        'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
5983
                    ],
5984
                    // User 2.
5985
                    [
5986
                        'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
5987
                        'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
5988
                    ]
5989
                ],
5990
                'expected' => [
5991
                    [
5992
                        'start' => $futurecoursestart,
5993
                        'startoffset' => 0
5994
                    ],
5995
                    [
5996
                        'start' => $futurecoursestart,
5997
                        'startoffset' => 0
5998
                    ]
5999
                ]
6000
            ],
6001
            'future course start fixed 2 users enrolled 2 methods 0 disabled 1 user suspended' => [
6002
                'relativedatemode' => false,
6003
                'coursestart' => $futurecoursestart,
6004
                'usercount' => 2,
6005
                'enrolmentmethods' => [
6006
                    ['manual', ENROL_INSTANCE_ENABLED],
6007
                    ['self', ENROL_INSTANCE_ENABLED]
6008
                ],
6009
                'enrolled' => [
6010
                    // User 1.
6011
                    [
6012
                        'manual' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED],
6013
                        'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6014
                    ],
6015
                    // User 2.
6016
                    [
6017
                        'manual' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED],
6018
                        'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6019
                    ]
6020
                ],
6021
                'expected' => [
6022
                    [
6023
                        'start' => $futurecoursestart,
6024
                        'startoffset' => 0
6025
                    ],
6026
                    [
6027
                        'start' => $futurecoursestart,
6028
                        'startoffset' => 0
6029
                    ]
6030
                ]
6031
            ],
6032
            'future course start fixed 2 users enrolled 2 methods 0 disabled 2 user suspended' => [
6033
                'relativedatemode' => false,
6034
                'coursestart' => $futurecoursestart,
6035
                'usercount' => 2,
6036
                'enrolmentmethods' => [
6037
                    ['manual', ENROL_INSTANCE_ENABLED],
6038
                    ['self', ENROL_INSTANCE_ENABLED]
6039
                ],
6040
                'enrolled' => [
6041
                    // User 1.
6042
                    [
6043
                        'manual' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED],
6044
                        'self' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED]
6045
                    ],
6046
                    // User 2.
6047
                    [
6048
                        'manual' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED],
6049
                        'self' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED]
6050
                    ]
6051
                ],
6052
                'expected' => [
6053
                    [
6054
                        'start' => $futurecoursestart,
6055
                        'startoffset' => 0
6056
                    ],
6057
                    [
6058
                        'start' => $futurecoursestart,
6059
                        'startoffset' => 0
6060
                    ]
6061
                ]
6062
            ],
6063
            'future course start relative no users enrolled' => [
6064
                'relativedatemode' => true,
6065
                'coursestart' => $futurecoursestart,
6066
                'usercount' => 2,
6067
                'enrolmentmethods' => [
6068
                    ['manual', ENROL_INSTANCE_ENABLED],
6069
                    ['self', ENROL_INSTANCE_ENABLED]
6070
                ],
6071
                'enrolled' => [[], []],
6072
                'expected' => [
6073
                    [
6074
                        'start' => $futurecoursestart,
6075
                        'startoffset' => 0
6076
                    ],
6077
                    [
6078
                        'start' => $futurecoursestart,
6079
                        'startoffset' => 0
6080
                    ]
6081
                ]
6082
            ],
6083
            'future course start relative 1 users enrolled future' => [
6084
                'relativedatemode' => true,
6085
                'coursestart' => $futurecoursestart,
6086
                'usercount' => 2,
6087
                'enrolmentmethods' => [
6088
                    ['manual', ENROL_INSTANCE_ENABLED],
6089
                    ['self', ENROL_INSTANCE_ENABLED]
6090
                ],
6091
                'enrolled' => [
6092
                    // User 1.
6093
                    ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
6094
                    // User 2.
6095
                    []
6096
                ],
6097
                'expected' => [
6098
                    [
6099
                        'start' => $futurecoursestart + 10,
6100
                        'startoffset' => 10
6101
                    ],
6102
                    [
6103
                        'start' => $futurecoursestart,
6104
                        'startoffset' => 0
6105
                    ]
6106
                ]
6107
            ],
6108
            'future course start relative 1 users enrolled past' => [
6109
                'relativedatemode' => true,
6110
                'coursestart' => $futurecoursestart,
6111
                'usercount' => 2,
6112
                'enrolmentmethods' => [
6113
                    ['manual', ENROL_INSTANCE_ENABLED],
6114
                    ['self', ENROL_INSTANCE_ENABLED]
6115
                ],
6116
                'enrolled' => [
6117
                    // User 1.
6118
                    ['manual' => [$futurecoursestart - 10, ENROL_USER_ACTIVE]],
6119
                    // User 2.
6120
                    []
6121
                ],
6122
                'expected' => [
6123
                    [
6124
                        'start' => $futurecoursestart,
6125
                        'startoffset' => 0
6126
                    ],
6127
                    [
6128
                        'start' => $futurecoursestart,
6129
                        'startoffset' => 0
6130
                    ]
6131
                ]
6132
            ],
6133
            'future course start relative 2 users enrolled future' => [
6134
                'relativedatemode' => true,
6135
                'coursestart' => $futurecoursestart,
6136
                'usercount' => 2,
6137
                'enrolmentmethods' => [
6138
                    ['manual', ENROL_INSTANCE_ENABLED],
6139
                    ['self', ENROL_INSTANCE_ENABLED]
6140
                ],
6141
                'enrolled' => [
6142
                    // User 1.
6143
                    ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
6144
                    // User 2.
6145
                    ['manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]]
6146
                ],
6147
                'expected' => [
6148
                    [
6149
                        'start' => $futurecoursestart + 10,
6150
                        'startoffset' => 10
6151
                    ],
6152
                    [
6153
                        'start' => $futurecoursestart + 20,
6154
                        'startoffset' => 20
6155
                    ]
6156
                ]
6157
            ],
6158
            'future course start relative 2 users enrolled past' => [
6159
                'relativedatemode' => true,
6160
                'coursestart' => $futurecoursestart,
6161
                'usercount' => 2,
6162
                'enrolmentmethods' => [
6163
                    ['manual', ENROL_INSTANCE_ENABLED],
6164
                    ['self', ENROL_INSTANCE_ENABLED]
6165
                ],
6166
                'enrolled' => [
6167
                    // User 1.
6168
                    ['manual' => [$futurecoursestart - 10, ENROL_USER_ACTIVE]],
6169
                    // User 2.
6170
                    ['manual' => [$futurecoursestart - 20, ENROL_USER_ACTIVE]]
6171
                ],
6172
                'expected' => [
6173
                    [
6174
                        'start' => $futurecoursestart,
6175
                        'startoffset' => 0
6176
                    ],
6177
                    [
6178
                        'start' => $futurecoursestart,
6179
                        'startoffset' => 0
6180
                    ]
6181
                ]
6182
            ],
6183
            'future course start relative 2 users enrolled mixed' => [
6184
                'relativedatemode' => true,
6185
                'coursestart' => $futurecoursestart,
6186
                'usercount' => 2,
6187
                'enrolmentmethods' => [
6188
                    ['manual', ENROL_INSTANCE_ENABLED],
6189
                    ['self', ENROL_INSTANCE_ENABLED]
6190
                ],
6191
                'enrolled' => [
6192
                    // User 1.
6193
                    ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
6194
                    // User 2.
6195
                    ['manual' => [$futurecoursestart - 20, ENROL_USER_ACTIVE]]
6196
                ],
6197
                'expected' => [
6198
                    [
6199
                        'start' => $futurecoursestart + 10,
6200
                        'startoffset' => 10
6201
                    ],
6202
                    [
6203
                        'start' => $futurecoursestart,
6204
                        'startoffset' => 0
6205
                    ]
6206
                ]
6207
            ],
6208
            'future course start relative 2 users enrolled 2 methods' => [
6209
                'relativedatemode' => true,
6210
                'coursestart' => $futurecoursestart,
6211
                'usercount' => 2,
6212
                'enrolmentmethods' => [
6213
                    ['manual', ENROL_INSTANCE_ENABLED],
6214
                    ['self', ENROL_INSTANCE_ENABLED]
6215
                ],
6216
                'enrolled' => [
6217
                    // User 1.
6218
                    [
6219
                        'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
6220
                        'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6221
                    ],
6222
                    // User 2.
6223
                    [
6224
                        'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
6225
                        'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6226
                    ]
6227
                ],
6228
                'expected' => [
6229
                    [
6230
                        'start' => $futurecoursestart + 10,
6231
                        'startoffset' => 10
6232
                    ],
6233
                    [
6234
                        'start' => $futurecoursestart + 10,
6235
                        'startoffset' => 10
6236
                    ]
6237
                ]
6238
            ],
6239
            'future course start relative 2 users enrolled 2 methods 1 disabled' => [
6240
                'relativedatemode' => true,
6241
                'coursestart' => $futurecoursestart,
6242
                'usercount' => 2,
6243
                'enrolmentmethods' => [
6244
                    ['manual', ENROL_INSTANCE_DISABLED],
6245
                    ['self', ENROL_INSTANCE_ENABLED]
6246
                ],
6247
                'enrolled' => [
6248
                    // User 1.
6249
                    [
6250
                        'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
6251
                        'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6252
                    ],
6253
                    // User 2.
6254
                    [
6255
                        'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
6256
                        'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6257
                    ]
6258
                ],
6259
                'expected' => [
6260
                    [
6261
                        'start' => $futurecoursestart + 20,
6262
                        'startoffset' => 20
6263
                    ],
6264
                    [
6265
                        'start' => $futurecoursestart + 10,
6266
                        'startoffset' => 10
6267
                    ]
6268
                ]
6269
            ],
6270
            'future course start relative 2 users enrolled 2 methods 2 disabled' => [
6271
                'relativedatemode' => true,
6272
                'coursestart' => $futurecoursestart,
6273
                'usercount' => 2,
6274
                'enrolmentmethods' => [
6275
                    ['manual', ENROL_INSTANCE_DISABLED],
6276
                    ['self', ENROL_INSTANCE_DISABLED]
6277
                ],
6278
                'enrolled' => [
6279
                    // User 1.
6280
                    [
6281
                        'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
6282
                        'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6283
                    ],
6284
                    // User 2.
6285
                    [
6286
                        'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
6287
                        'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6288
                    ]
6289
                ],
6290
                'expected' => [
6291
                    [
6292
                        'start' => $futurecoursestart,
6293
                        'startoffset' => 0
6294
                    ],
6295
                    [
6296
                        'start' => $futurecoursestart,
6297
                        'startoffset' => 0
6298
                    ]
6299
                ]
6300
            ],
6301
            'future course start relative 2 users enrolled 2 methods 0 disabled 1 user suspended' => [
6302
                'relativedatemode' => true,
6303
                'coursestart' => $futurecoursestart,
6304
                'usercount' => 2,
6305
                'enrolmentmethods' => [
6306
                    ['manual', ENROL_INSTANCE_ENABLED],
6307
                    ['self', ENROL_INSTANCE_ENABLED]
6308
                ],
6309
                'enrolled' => [
6310
                    // User 1.
6311
                    [
6312
                        'manual' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED],
6313
                        'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6314
                    ],
6315
                    // User 2.
6316
                    [
6317
                        'manual' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED],
6318
                        'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6319
                    ]
6320
                ],
6321
                'expected' => [
6322
                    [
6323
                        'start' => $futurecoursestart + 20,
6324
                        'startoffset' => 20
6325
                    ],
6326
                    [
6327
                        'start' => $futurecoursestart + 10,
6328
                        'startoffset' => 10
6329
                    ]
6330
                ]
6331
            ],
6332
            'future course start relative 2 users enrolled 2 methods 0 disabled 2 user suspended' => [
6333
                'relativedatemode' => true,
6334
                'coursestart' => $futurecoursestart,
6335
                'usercount' => 2,
6336
                'enrolmentmethods' => [
6337
                    ['manual', ENROL_INSTANCE_ENABLED],
6338
                    ['self', ENROL_INSTANCE_ENABLED]
6339
                ],
6340
                'enrolled' => [
6341
                    // User 1.
6342
                    [
6343
                        'manual' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED],
6344
                        'self' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED]
6345
                    ],
6346
                    // User 2.
6347
                    [
6348
                        'manual' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED],
6349
                        'self' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED]
6350
                    ]
6351
                ],
6352
                'expected' => [
6353
                    [
6354
                        'start' => $futurecoursestart,
6355
                        'startoffset' => 0
6356
                    ],
6357
                    [
6358
                        'start' => $futurecoursestart,
6359
                        'startoffset' => 0
6360
                    ]
6361
                ]
6362
            ],
6363
 
6364
            // Course start date in the past.
6365
            'past course start fixed no users enrolled' => [
6366
                'relativedatemode' => false,
6367
                'coursestart' => $pastcoursestart,
6368
                'usercount' => 2,
6369
                'enrolmentmethods' => [
6370
                    ['manual', ENROL_INSTANCE_ENABLED],
6371
                    ['self', ENROL_INSTANCE_ENABLED]
6372
                ],
6373
                'enrolled' => [[], []],
6374
                'expected' => [
6375
                    [
6376
                        'start' => $pastcoursestart,
6377
                        'startoffset' => 0
6378
                    ],
6379
                    [
6380
                        'start' => $pastcoursestart,
6381
                        'startoffset' => 0
6382
                    ]
6383
                ]
6384
            ],
6385
            'past course start fixed 1 users enrolled future' => [
6386
                'relativedatemode' => false,
6387
                'coursestart' => $pastcoursestart,
6388
                'usercount' => 2,
6389
                'enrolmentmethods' => [
6390
                    ['manual', ENROL_INSTANCE_ENABLED],
6391
                    ['self', ENROL_INSTANCE_ENABLED]
6392
                ],
6393
                'enrolled' => [
6394
                    // User 1.
6395
                    ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6396
                    // User 2.
6397
                    []
6398
                ],
6399
                'expected' => [
6400
                    [
6401
                        'start' => $pastcoursestart,
6402
                        'startoffset' => 0
6403
                    ],
6404
                    [
6405
                        'start' => $pastcoursestart,
6406
                        'startoffset' => 0
6407
                    ]
6408
                ]
6409
            ],
6410
            'past course start fixed 1 users enrolled past' => [
6411
                'relativedatemode' => false,
6412
                'coursestart' => $pastcoursestart,
6413
                'usercount' => 2,
6414
                'enrolmentmethods' => [
6415
                    ['manual', ENROL_INSTANCE_ENABLED],
6416
                    ['self', ENROL_INSTANCE_ENABLED]
6417
                ],
6418
                'enrolled' => [
6419
                    // User 1.
6420
                    ['manual' => [$pastcoursestart - 10, ENROL_USER_ACTIVE]],
6421
                    // User 2.
6422
                    []
6423
                ],
6424
                'expected' => [
6425
                    [
6426
                        'start' => $pastcoursestart,
6427
                        'startoffset' => 0
6428
                    ],
6429
                    [
6430
                        'start' => $pastcoursestart,
6431
                        'startoffset' => 0
6432
                    ]
6433
                ]
6434
            ],
6435
            'past course start fixed 2 users enrolled future' => [
6436
                'relativedatemode' => false,
6437
                'coursestart' => $pastcoursestart,
6438
                'usercount' => 2,
6439
                'enrolmentmethods' => [
6440
                    ['manual', ENROL_INSTANCE_ENABLED],
6441
                    ['self', ENROL_INSTANCE_ENABLED]
6442
                ],
6443
                'enrolled' => [
6444
                    // User 1.
6445
                    ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6446
                    // User 2.
6447
                    ['manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]]
6448
                ],
6449
                'expected' => [
6450
                    [
6451
                        'start' => $pastcoursestart,
6452
                        'startoffset' => 0
6453
                    ],
6454
                    [
6455
                        'start' => $pastcoursestart,
6456
                        'startoffset' => 0
6457
                    ]
6458
                ]
6459
            ],
6460
            'past course start fixed 2 users enrolled past' => [
6461
                'relativedatemode' => false,
6462
                'coursestart' => $pastcoursestart,
6463
                'usercount' => 2,
6464
                'enrolmentmethods' => [
6465
                    ['manual', ENROL_INSTANCE_ENABLED],
6466
                    ['self', ENROL_INSTANCE_ENABLED]
6467
                ],
6468
                'enrolled' => [
6469
                    // User 1.
6470
                    ['manual' => [$pastcoursestart - 10, ENROL_USER_ACTIVE]],
6471
                    // User 2.
6472
                    ['manual' => [$pastcoursestart - 20, ENROL_USER_ACTIVE]]
6473
                ],
6474
                'expected' => [
6475
                    [
6476
                        'start' => $pastcoursestart,
6477
                        'startoffset' => 0
6478
                    ],
6479
                    [
6480
                        'start' => $pastcoursestart,
6481
                        'startoffset' => 0
6482
                    ]
6483
                ]
6484
            ],
6485
            'past course start fixed 2 users enrolled mixed' => [
6486
                'relativedatemode' => false,
6487
                'coursestart' => $pastcoursestart,
6488
                'usercount' => 2,
6489
                'enrolmentmethods' => [
6490
                    ['manual', ENROL_INSTANCE_ENABLED],
6491
                    ['self', ENROL_INSTANCE_ENABLED]
6492
                ],
6493
                'enrolled' => [
6494
                    // User 1.
6495
                    ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6496
                    // User 2.
6497
                    ['manual' => [$pastcoursestart - 20, ENROL_USER_ACTIVE]]
6498
                ],
6499
                'expected' => [
6500
                    [
6501
                        'start' => $pastcoursestart,
6502
                        'startoffset' => 0
6503
                    ],
6504
                    [
6505
                        'start' => $pastcoursestart,
6506
                        'startoffset' => 0
6507
                    ]
6508
                ]
6509
            ],
6510
            'past course start fixed 2 users enrolled 2 methods' => [
6511
                'relativedatemode' => false,
6512
                'coursestart' => $pastcoursestart,
6513
                'usercount' => 2,
6514
                'enrolmentmethods' => [
6515
                    ['manual', ENROL_INSTANCE_ENABLED],
6516
                    ['self', ENROL_INSTANCE_ENABLED]
6517
                ],
6518
                'enrolled' => [
6519
                    // User 1.
6520
                    [
6521
                        'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6522
                        'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6523
                    ],
6524
                    // User 2.
6525
                    [
6526
                        'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6527
                        'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6528
                    ]
6529
                ],
6530
                'expected' => [
6531
                    [
6532
                        'start' => $pastcoursestart,
6533
                        'startoffset' => 0
6534
                    ],
6535
                    [
6536
                        'start' => $pastcoursestart,
6537
                        'startoffset' => 0
6538
                    ]
6539
                ]
6540
            ],
6541
            'past course start fixed 2 users enrolled 2 methods 1 disabled' => [
6542
                'relativedatemode' => false,
6543
                'coursestart' => $pastcoursestart,
6544
                'usercount' => 2,
6545
                'enrolmentmethods' => [
6546
                    ['manual', ENROL_INSTANCE_DISABLED],
6547
                    ['self', ENROL_INSTANCE_ENABLED]
6548
                ],
6549
                'enrolled' => [
6550
                    // User 1.
6551
                    [
6552
                        'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6553
                        'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6554
                    ],
6555
                    // User 2.
6556
                    [
6557
                        'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6558
                        'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6559
                    ]
6560
                ],
6561
                'expected' => [
6562
                    [
6563
                        'start' => $pastcoursestart,
6564
                        'startoffset' => 0
6565
                    ],
6566
                    [
6567
                        'start' => $pastcoursestart,
6568
                        'startoffset' => 0
6569
                    ]
6570
                ]
6571
            ],
6572
            'past course start fixed 2 users enrolled 2 methods 2 disabled' => [
6573
                'relativedatemode' => false,
6574
                'coursestart' => $pastcoursestart,
6575
                'usercount' => 2,
6576
                'enrolmentmethods' => [
6577
                    ['manual', ENROL_INSTANCE_DISABLED],
6578
                    ['self', ENROL_INSTANCE_DISABLED]
6579
                ],
6580
                'enrolled' => [
6581
                    // User 1.
6582
                    [
6583
                        'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6584
                        'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6585
                    ],
6586
                    // User 2.
6587
                    [
6588
                        'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6589
                        'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6590
                    ]
6591
                ],
6592
                'expected' => [
6593
                    [
6594
                        'start' => $pastcoursestart,
6595
                        'startoffset' => 0
6596
                    ],
6597
                    [
6598
                        'start' => $pastcoursestart,
6599
                        'startoffset' => 0
6600
                    ]
6601
                ]
6602
            ],
6603
            'past course start fixed 2 users enrolled 2 methods 0 disabled 1 user suspended' => [
6604
                'relativedatemode' => false,
6605
                'coursestart' => $pastcoursestart,
6606
                'usercount' => 2,
6607
                'enrolmentmethods' => [
6608
                    ['manual', ENROL_INSTANCE_ENABLED],
6609
                    ['self', ENROL_INSTANCE_ENABLED]
6610
                ],
6611
                'enrolled' => [
6612
                    // User 1.
6613
                    [
6614
                        'manual' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED],
6615
                        'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6616
                    ],
6617
                    // User 2.
6618
                    [
6619
                        'manual' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED],
6620
                        'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6621
                    ]
6622
                ],
6623
                'expected' => [
6624
                    [
6625
                        'start' => $pastcoursestart,
6626
                        'startoffset' => 0
6627
                    ],
6628
                    [
6629
                        'start' => $pastcoursestart,
6630
                        'startoffset' => 0
6631
                    ]
6632
                ]
6633
            ],
6634
            'past course start fixed 2 users enrolled 2 methods 0 disabled 2 user suspended' => [
6635
                'relativedatemode' => false,
6636
                'coursestart' => $pastcoursestart,
6637
                'usercount' => 2,
6638
                'enrolmentmethods' => [
6639
                    ['manual', ENROL_INSTANCE_ENABLED],
6640
                    ['self', ENROL_INSTANCE_ENABLED]
6641
                ],
6642
                'enrolled' => [
6643
                    // User 1.
6644
                    [
6645
                        'manual' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED],
6646
                        'self' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED]
6647
                    ],
6648
                    // User 2.
6649
                    [
6650
                        'manual' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED],
6651
                        'self' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED]
6652
                    ]
6653
                ],
6654
                'expected' => [
6655
                    [
6656
                        'start' => $pastcoursestart,
6657
                        'startoffset' => 0
6658
                    ],
6659
                    [
6660
                        'start' => $pastcoursestart,
6661
                        'startoffset' => 0
6662
                    ]
6663
                ]
6664
            ],
6665
            'past course start relative no users enrolled' => [
6666
                'relativedatemode' => true,
6667
                'coursestart' => $pastcoursestart,
6668
                'usercount' => 2,
6669
                'enrolmentmethods' => [
6670
                    ['manual', ENROL_INSTANCE_ENABLED],
6671
                    ['self', ENROL_INSTANCE_ENABLED]
6672
                ],
6673
                'enrolled' => [[], []],
6674
                'expected' => [
6675
                    [
6676
                        'start' => $pastcoursestart,
6677
                        'startoffset' => 0
6678
                    ],
6679
                    [
6680
                        'start' => $pastcoursestart,
6681
                        'startoffset' => 0
6682
                    ]
6683
                ]
6684
            ],
6685
            'past course start relative 1 users enrolled future' => [
6686
                'relativedatemode' => true,
6687
                'coursestart' => $pastcoursestart,
6688
                'usercount' => 2,
6689
                'enrolmentmethods' => [
6690
                    ['manual', ENROL_INSTANCE_ENABLED],
6691
                    ['self', ENROL_INSTANCE_ENABLED]
6692
                ],
6693
                'enrolled' => [
6694
                    // User 1.
6695
                    ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6696
                    // User 2.
6697
                    []
6698
                ],
6699
                'expected' => [
6700
                    [
6701
                        'start' => $pastcoursestart + 10,
6702
                        'startoffset' => 10
6703
                    ],
6704
                    [
6705
                        'start' => $pastcoursestart,
6706
                        'startoffset' => 0
6707
                    ]
6708
                ]
6709
            ],
6710
            'past course start relative 1 users enrolled past' => [
6711
                'relativedatemode' => true,
6712
                'coursestart' => $pastcoursestart,
6713
                'usercount' => 2,
6714
                'enrolmentmethods' => [
6715
                    ['manual', ENROL_INSTANCE_ENABLED],
6716
                    ['self', ENROL_INSTANCE_ENABLED]
6717
                ],
6718
                'enrolled' => [
6719
                    // User 1.
6720
                    ['manual' => [$pastcoursestart - 10, ENROL_USER_ACTIVE]],
6721
                    // User 2.
6722
                    []
6723
                ],
6724
                'expected' => [
6725
                    [
6726
                        'start' => $pastcoursestart,
6727
                        'startoffset' => 0
6728
                    ],
6729
                    [
6730
                        'start' => $pastcoursestart,
6731
                        'startoffset' => 0
6732
                    ]
6733
                ]
6734
            ],
6735
            'past course start relative 2 users enrolled future' => [
6736
                'relativedatemode' => true,
6737
                'coursestart' => $pastcoursestart,
6738
                'usercount' => 2,
6739
                'enrolmentmethods' => [
6740
                    ['manual', ENROL_INSTANCE_ENABLED],
6741
                    ['self', ENROL_INSTANCE_ENABLED]
6742
                ],
6743
                'enrolled' => [
6744
                    // User 1.
6745
                    ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6746
                    // User 2.
6747
                    ['manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]]
6748
                ],
6749
                'expected' => [
6750
                    [
6751
                        'start' => $pastcoursestart + 10,
6752
                        'startoffset' => 10
6753
                    ],
6754
                    [
6755
                        'start' => $pastcoursestart + 20,
6756
                        'startoffset' => 20
6757
                    ]
6758
                ]
6759
            ],
6760
            'past course start relative 2 users enrolled past' => [
6761
                'relativedatemode' => true,
6762
                'coursestart' => $pastcoursestart,
6763
                'usercount' => 2,
6764
                'enrolmentmethods' => [
6765
                    ['manual', ENROL_INSTANCE_ENABLED],
6766
                    ['self', ENROL_INSTANCE_ENABLED]
6767
                ],
6768
                'enrolled' => [
6769
                    // User 1.
6770
                    ['manual' => [$pastcoursestart - 10, ENROL_USER_ACTIVE]],
6771
                    // User 2.
6772
                    ['manual' => [$pastcoursestart - 20, ENROL_USER_ACTIVE]]
6773
                ],
6774
                'expected' => [
6775
                    [
6776
                        'start' => $pastcoursestart,
6777
                        'startoffset' => 0
6778
                    ],
6779
                    [
6780
                        'start' => $pastcoursestart,
6781
                        'startoffset' => 0
6782
                    ]
6783
                ]
6784
            ],
6785
            'past course start relative 2 users enrolled mixed' => [
6786
                'relativedatemode' => true,
6787
                'coursestart' => $pastcoursestart,
6788
                'usercount' => 2,
6789
                'enrolmentmethods' => [
6790
                    ['manual', ENROL_INSTANCE_ENABLED],
6791
                    ['self', ENROL_INSTANCE_ENABLED]
6792
                ],
6793
                'enrolled' => [
6794
                    // User 1.
6795
                    ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6796
                    // User 2.
6797
                    ['manual' => [$pastcoursestart - 20, ENROL_USER_ACTIVE]]
6798
                ],
6799
                'expected' => [
6800
                    [
6801
                        'start' => $pastcoursestart + 10,
6802
                        'startoffset' => 10
6803
                    ],
6804
                    [
6805
                        'start' => $pastcoursestart,
6806
                        'startoffset' => 0
6807
                    ]
6808
                ]
6809
            ],
6810
            'past course start relative 2 users enrolled 2 methods' => [
6811
                'relativedatemode' => true,
6812
                'coursestart' => $pastcoursestart,
6813
                'usercount' => 2,
6814
                'enrolmentmethods' => [
6815
                    ['manual', ENROL_INSTANCE_ENABLED],
6816
                    ['self', ENROL_INSTANCE_ENABLED]
6817
                ],
6818
                'enrolled' => [
6819
                    // User 1.
6820
                    [
6821
                        'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6822
                        'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6823
                    ],
6824
                    // User 2.
6825
                    [
6826
                        'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6827
                        'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6828
                    ]
6829
                ],
6830
                'expected' => [
6831
                    [
6832
                        'start' => $pastcoursestart + 10,
6833
                        'startoffset' => 10
6834
                    ],
6835
                    [
6836
                        'start' => $pastcoursestart + 10,
6837
                        'startoffset' => 10
6838
                    ]
6839
                ]
6840
            ],
6841
            'past course start relative 2 users enrolled 2 methods 1 disabled' => [
6842
                'relativedatemode' => true,
6843
                'coursestart' => $pastcoursestart,
6844
                'usercount' => 2,
6845
                'enrolmentmethods' => [
6846
                    ['manual', ENROL_INSTANCE_DISABLED],
6847
                    ['self', ENROL_INSTANCE_ENABLED]
6848
                ],
6849
                'enrolled' => [
6850
                    // User 1.
6851
                    [
6852
                        'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6853
                        'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6854
                    ],
6855
                    // User 2.
6856
                    [
6857
                        'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6858
                        'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6859
                    ]
6860
                ],
6861
                'expected' => [
6862
                    [
6863
                        'start' => $pastcoursestart + 20,
6864
                        'startoffset' => 20
6865
                    ],
6866
                    [
6867
                        'start' => $pastcoursestart + 10,
6868
                        'startoffset' => 10
6869
                    ]
6870
                ]
6871
            ],
6872
            'past course start relative 2 users enrolled 2 methods 2 disabled' => [
6873
                'relativedatemode' => true,
6874
                'coursestart' => $pastcoursestart,
6875
                'usercount' => 2,
6876
                'enrolmentmethods' => [
6877
                    ['manual', ENROL_INSTANCE_DISABLED],
6878
                    ['self', ENROL_INSTANCE_DISABLED]
6879
                ],
6880
                'enrolled' => [
6881
                    // User 1.
6882
                    [
6883
                        'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6884
                        'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6885
                    ],
6886
                    // User 2.
6887
                    [
6888
                        'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6889
                        'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6890
                    ]
6891
                ],
6892
                'expected' => [
6893
                    [
6894
                        'start' => $pastcoursestart,
6895
                        'startoffset' => 0
6896
                    ],
6897
                    [
6898
                        'start' => $pastcoursestart,
6899
                        'startoffset' => 0
6900
                    ]
6901
                ]
6902
            ],
6903
            'past course start relative 2 users enrolled 2 methods 0 disabled 1 user suspended' => [
6904
                'relativedatemode' => true,
6905
                'coursestart' => $pastcoursestart,
6906
                'usercount' => 2,
6907
                'enrolmentmethods' => [
6908
                    ['manual', ENROL_INSTANCE_ENABLED],
6909
                    ['self', ENROL_INSTANCE_ENABLED]
6910
                ],
6911
                'enrolled' => [
6912
                    // User 1.
6913
                    [
6914
                        'manual' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED],
6915
                        'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6916
                    ],
6917
                    // User 2.
6918
                    [
6919
                        'manual' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED],
6920
                        'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6921
                    ]
6922
                ],
6923
                'expected' => [
6924
                    [
6925
                        'start' => $pastcoursestart + 20,
6926
                        'startoffset' => 20
6927
                    ],
6928
                    [
6929
                        'start' => $pastcoursestart + 10,
6930
                        'startoffset' => 10
6931
                    ]
6932
                ]
6933
            ],
6934
            'past course start relative 2 users enrolled 2 methods 0 disabled 2 user suspended' => [
6935
                'relativedatemode' => true,
6936
                'coursestart' => $pastcoursestart,
6937
                'usercount' => 2,
6938
                'enrolmentmethods' => [
6939
                    ['manual', ENROL_INSTANCE_ENABLED],
6940
                    ['self', ENROL_INSTANCE_ENABLED]
6941
                ],
6942
                'enrolled' => [
6943
                    // User 1.
6944
                    [
6945
                        'manual' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED],
6946
                        'self' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED]
6947
                    ],
6948
                    // User 2.
6949
                    [
6950
                        'manual' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED],
6951
                        'self' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED]
6952
                    ]
6953
                ],
6954
                'expected' => [
6955
                    [
6956
                        'start' => $pastcoursestart,
6957
                        'startoffset' => 0
6958
                    ],
6959
                    [
6960
                        'start' => $pastcoursestart,
6961
                        'startoffset' => 0
6962
                    ]
6963
                ]
6964
            ]
6965
        ];
6966
    }
6967
 
6968
    /**
6969
     * Test the course_get_course_dates_for_user_ids function.
6970
     *
6971
     * @dataProvider get_course_get_course_dates_for_user_ids_test_cases()
6972
     * @param bool $relativedatemode Set the course to relative dates mode
6973
     * @param int $coursestart Course start date
6974
     * @param int $usercount Number of users to create
6975
     * @param array $enrolmentmethods Enrolment methods to set for the course
6976
     * @param array $enrolled Enrolment config for to set for the users
6977
     * @param array $expected Expected output
6978
     */
6979
    public function test_course_get_course_dates_for_user_ids(
6980
        $relativedatemode,
6981
        $coursestart,
6982
        $usercount,
6983
        $enrolmentmethods,
6984
        $enrolled,
6985
        $expected
6986
    ) {
6987
        global $DB;
6988
        $this->resetAfterTest();
6989
 
6990
        $generator = $this->getDataGenerator();
6991
        $course  = $generator->create_course(['startdate' => $coursestart]);
6992
        $course->relativedatesmode = $relativedatemode;
6993
        $users = [];
6994
 
6995
        for ($i = 0; $i < $usercount; $i++) {
6996
            $users[] = $generator->create_user();
6997
        }
6998
 
6999
        foreach ($enrolmentmethods as [$type, $status]) {
7000
            $record = $DB->get_record('enrol', ['courseid' => $course->id, 'enrol' => $type]);
7001
            $plugin = enrol_get_plugin($type);
7002
            if ($record->status != $status) {
7003
                $plugin->update_status($record, $status);
7004
            }
7005
        }
7006
 
7007
        foreach ($enrolled as $index => $enrolconfig) {
7008
            $user = $users[$index];
7009
            foreach ($enrolconfig as $type => [$starttime, $status]) {
7010
                $generator->enrol_user($user->id, $course->id, 'student', $type, $starttime, 0, $status);
7011
            }
7012
        }
7013
 
7014
        $userids = array_map(function($user) {
7015
            return $user->id;
7016
        }, $users);
7017
        $actual = course_get_course_dates_for_user_ids($course, $userids);
7018
 
7019
        foreach ($expected as $index => $exp) {
7020
            $userid = $userids[$index];
7021
            $act = $actual[$userid];
7022
 
7023
            $this->assertEquals($exp['start'], $act['start']);
7024
            $this->assertEquals($exp['startoffset'], $act['startoffset']);
7025
        }
7026
    }
7027
 
7028
    /**
7029
     * Test that calling course_get_course_dates_for_user_ids multiple times in the
7030
     * same request fill fetch the correct data for the user.
7031
     */
7032
    public function test_course_get_course_dates_for_user_ids_multiple_calls() {
7033
        $this->resetAfterTest();
7034
 
7035
        $generator = $this->getDataGenerator();
7036
        $now = time();
7037
        $coursestart = $now - 1000;
7038
        $course  = $generator->create_course(['startdate' => $coursestart]);
7039
        $course->relativedatesmode = true;
7040
        $user1 = $generator->create_user();
7041
        $user2 = $generator->create_user();
7042
        $user1start = $coursestart + 100;
7043
        $user2start = $coursestart + 200;
7044
 
7045
        $generator->enrol_user($user1->id, $course->id, 'student', 'manual', $user1start);
7046
        $generator->enrol_user($user2->id, $course->id, 'student', 'manual', $user2start);
7047
 
7048
        $result = course_get_course_dates_for_user_ids($course, [$user1->id]);
7049
        $this->assertEquals($user1start, $result[$user1->id]['start']);
7050
 
7051
        $result = course_get_course_dates_for_user_ids($course, [$user1->id, $user2->id]);
7052
        $this->assertEquals($user1start, $result[$user1->id]['start']);
7053
        $this->assertEquals($user2start, $result[$user2->id]['start']);
7054
 
7055
        $result = course_get_course_dates_for_user_ids($course, [$user2->id]);
7056
        $this->assertEquals($user2start, $result[$user2->id]['start']);
7057
    }
7058
 
7059
    /**
7060
     * Data provider for test_course_modules_pending_deletion.
7061
     *
7062
     * @return array An array of arrays contain test data
7063
     */
7064
    public function provider_course_modules_pending_deletion() {
7065
        return [
7066
            'Non-gradable activity, check all'              => [['forum'], 0, false, true],
7067
            'Gradable activity, check all'                  => [['assign'], 0, false, true],
7068
            'Non-gradable activity, check gradables'        => [['forum'], 0, true, false],
7069
            'Gradable activity, check gradables'            => [['assign'], 0, true, true],
7070
            'Non-gradable within multiple, check all'       => [['quiz', 'forum', 'assign'], 1, false, true],
7071
            'Non-gradable within multiple, check gradables' => [['quiz', 'forum', 'assign'], 1, true, false],
7072
            'Gradable within multiple, check all'           => [['quiz', 'forum', 'assign'], 2, false, true],
7073
            'Gradable within multiple, check gradables'     => [['quiz', 'forum', 'assign'], 2, true, true],
7074
        ];
7075
    }
7076
 
7077
    /**
7078
     * Tests the function course_modules_pending_deletion.
7079
     *
7080
     * @param string[] $modules A complete list aff all available modules before deletion
7081
     * @param int $indextodelete The index of the module in the $modules array that we want to test with
7082
     * @param bool $gradable The value to pass to the gradable argument of the course_modules_pending_deletion function
7083
     * @param bool $expected The expected result
7084
     * @dataProvider provider_course_modules_pending_deletion
7085
     */
7086
    public function test_course_modules_pending_deletion(array $modules, int $indextodelete, bool $gradable, bool $expected) {
7087
        $this->resetAfterTest();
7088
 
7089
        // Ensure recyclebin is enabled.
7090
        set_config('coursebinenable', true, 'tool_recyclebin');
7091
 
7092
        // Create course and modules.
7093
        $generator = $this->getDataGenerator();
7094
        $course = $generator->create_course();
7095
 
7096
        $moduleinstances = [];
7097
        foreach ($modules as $module) {
7098
            $moduleinstances[] = $generator->create_module($module, array('course' => $course->id));
7099
        }
7100
 
7101
        course_delete_module($moduleinstances[$indextodelete]->cmid, true); // Try to delete the instance asynchronously.
7102
        $this->assertEquals($expected, course_modules_pending_deletion($course->id, $gradable));
7103
    }
7104
 
7105
    /**
7106
     * Tests for the course_request::can_request
7107
     */
7108
    public function test_can_request_course() {
7109
        global $CFG, $DB;
7110
        $this->resetAfterTest();
7111
 
7112
        $user = $this->getDataGenerator()->create_user();
7113
        $cat1 = $CFG->defaultrequestcategory;
7114
        $cat2 = $this->getDataGenerator()->create_category()->id;
7115
        $cat3 = $this->getDataGenerator()->create_category()->id;
7116
        $context1 = context_coursecat::instance($cat1);
7117
        $context2 = context_coursecat::instance($cat2);
7118
        $context3 = context_coursecat::instance($cat3);
7119
        $this->setUser($user);
7120
 
7121
        // By default users don't have capability to request courses.
7122
        $this->assertFalse(course_request::can_request(context_system::instance()));
7123
        $this->assertFalse(course_request::can_request($context1));
7124
        $this->assertFalse(course_request::can_request($context2));
7125
        $this->assertFalse(course_request::can_request($context3));
7126
 
7127
        // Allow for the 'user' role the capability to request courses.
7128
        $userroleid = $DB->get_field('role', 'id', ['shortname' => 'user']);
7129
        assign_capability('moodle/course:request', CAP_ALLOW, $userroleid,
7130
            context_system::instance()->id);
7131
        accesslib_clear_all_caches_for_unit_testing();
7132
 
7133
        // Lock category selection.
7134
        $CFG->lockrequestcategory = 1;
7135
 
7136
        // Now user can only request course in the default category or in system context.
7137
        $this->assertTrue(course_request::can_request(context_system::instance()));
7138
        $this->assertTrue(course_request::can_request($context1));
7139
        $this->assertFalse(course_request::can_request($context2));
7140
        $this->assertFalse(course_request::can_request($context3));
7141
 
7142
        // Enable category selection. User can request course anywhere.
7143
        $CFG->lockrequestcategory = 0;
7144
        $this->assertTrue(course_request::can_request(context_system::instance()));
7145
        $this->assertTrue(course_request::can_request($context1));
7146
        $this->assertTrue(course_request::can_request($context2));
7147
        $this->assertTrue(course_request::can_request($context3));
7148
 
7149
        // Remove cap from cat2.
7150
        $roleid = create_role('Test role', 'testrole', 'Test role description');
7151
        assign_capability('moodle/course:request', CAP_PROHIBIT, $roleid,
7152
            $context2->id, true);
7153
        role_assign($roleid, $user->id, $context2->id);
7154
        accesslib_clear_all_caches_for_unit_testing();
7155
 
7156
        $this->assertTrue(course_request::can_request(context_system::instance()));
7157
        $this->assertTrue(course_request::can_request($context1));
7158
        $this->assertFalse(course_request::can_request($context2));
7159
        $this->assertTrue(course_request::can_request($context3));
7160
 
7161
        // Disable course request functionality.
7162
        $CFG->enablecourserequests = false;
7163
        $this->assertFalse(course_request::can_request(context_system::instance()));
7164
        $this->assertFalse(course_request::can_request($context1));
7165
        $this->assertFalse(course_request::can_request($context2));
7166
        $this->assertFalse(course_request::can_request($context3));
7167
    }
7168
 
7169
    /**
7170
     * Tests for the course_request::can_approve
7171
     */
7172
    public function test_can_approve_course_request() {
7173
        global $CFG;
7174
        $this->resetAfterTest();
7175
 
7176
        $requestor = $this->getDataGenerator()->create_user();
7177
        $user = $this->getDataGenerator()->create_user();
7178
        $cat1 = $CFG->defaultrequestcategory;
7179
        $cat2 = $this->getDataGenerator()->create_category()->id;
7180
        $cat3 = $this->getDataGenerator()->create_category()->id;
7181
 
7182
        // Enable course requests. Default 'user' role has capability to request courses.
7183
        $CFG->enablecourserequests = true;
7184
        $CFG->lockrequestcategory = 0;
7185
        $this->setUser($requestor);
7186
        $requestdata = ['summary_editor' => ['text' => '', 'format' => 0], 'name' => 'Req', 'reason' => 'test'];
7187
        $request1 = course_request::create((object)($requestdata));
7188
        $request2 = course_request::create((object)($requestdata + ['category' => $cat2]));
7189
        $request3 = course_request::create((object)($requestdata + ['category' => $cat3]));
7190
 
7191
        $this->setUser($user);
7192
        // Add capability to approve courses.
7193
        $roleid = create_role('Test role', 'testrole', 'Test role description');
7194
        assign_capability('moodle/site:approvecourse', CAP_ALLOW, $roleid,
7195
            context_system::instance()->id, true);
7196
        role_assign($roleid, $user->id, context_coursecat::instance($cat2)->id);
7197
        accesslib_clear_all_caches_for_unit_testing();
7198
 
7199
        $this->assertFalse($request1->can_approve());
7200
        $this->assertTrue($request2->can_approve());
7201
        $this->assertFalse($request3->can_approve());
7202
 
7203
        // Delete category where course was requested. Now only site-wide manager can approve it.
7204
        core_course_category::get($cat2, MUST_EXIST, true)->delete_full(false);
7205
        $this->assertFalse($request2->can_approve());
7206
 
7207
        $this->setAdminUser();
7208
        $this->assertTrue($request2->can_approve());
7209
    }
7210
 
7211
    /**
7212
     * Test the course allowed module method.
7213
     */
7214
    public function test_course_allowed_module() {
7215
        $this->resetAfterTest();
7216
        global $DB;
7217
 
7218
        $course = $this->getDataGenerator()->create_course();
7219
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
7220
        $manager = $this->getDataGenerator()->create_and_enrol($course, 'manager');
7221
 
7222
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
7223
        assign_capability('mod/assign:addinstance', CAP_PROHIBIT, $teacherrole->id, \context_course::instance($course->id));
7224
 
7225
        // Global user (teacher) has no permissions in this course.
7226
        $this->setUser($teacher);
7227
        $this->assertFalse(course_allowed_module($course, 'assign'));
7228
 
7229
        // Manager has permissions.
7230
        $this->assertTrue(course_allowed_module($course, 'assign', $manager));
7231
    }
7232
 
7233
    /**
7234
     * Test the {@link average_number_of_participants()} function.
7235
     */
7236
    public function test_average_number_of_participants() {
7237
        global $DB;
7238
        $this->resetAfterTest(true);
7239
 
7240
        $generator = $this->getDataGenerator();
7241
        $now = time();
7242
 
7243
        // If there are no courses, expect zero number of participants per course.
7244
        $this->assertEquals(0, average_number_of_participants());
7245
 
7246
        $c1 = $generator->create_course();
7247
        $c2 = $generator->create_course();
7248
 
7249
        // If there are no users, expect zero number of participants per course.
7250
        $this->assertEquals(0, average_number_of_participants());
7251
 
7252
        $t1 = $generator->create_user(['lastlogin' => $now]);
7253
        $s1 = $generator->create_user(['lastlogin' => $now]);
7254
        $s2 = $generator->create_user(['lastlogin' => $now - WEEKSECS]);
7255
        $s3 = $generator->create_user(['lastlogin' => $now - WEEKSECS]);
7256
        $s4 = $generator->create_user(['lastlogin' => $now - YEARSECS]);
7257
 
7258
        // We have courses, we have users, but no enrolments yet.
7259
        $this->assertEquals(0, average_number_of_participants());
7260
 
7261
        // Front page enrolments are ignored.
7262
        $generator->enrol_user($t1->id, SITEID, 'teacher');
7263
        $this->assertEquals(0, average_number_of_participants());
7264
 
7265
        // The teacher enrolled into one of the two courses.
7266
        $generator->enrol_user($t1->id, $c1->id, 'editingteacher');
7267
        $this->assertEquals(0.5, average_number_of_participants());
7268
 
7269
        // The teacher enrolled into both courses.
7270
        $generator->enrol_user($t1->id, $c2->id, 'editingteacher');
7271
        $this->assertEquals(1, average_number_of_participants());
7272
 
7273
        // Student 1 enrolled in the Course 1 only.
7274
        $generator->enrol_user($s1->id, $c1->id, 'student');
7275
        $this->assertEquals(1.5, average_number_of_participants());
7276
 
7277
        // Student 2 enrolled in both courses, but the enrolment in the Course 2 not active yet (enrolment starts in the future).
7278
        $generator->enrol_user($s2->id, $c1->id, 'student');
7279
        $generator->enrol_user($s2->id, $c2->id, 'student', 'manual', $now + WEEKSECS);
7280
        $this->assertEquals(2.5, average_number_of_participants());
7281
        $this->assertEquals(2, average_number_of_participants(true));
7282
 
7283
        // Student 3 enrolled in the Course 1, but the enrolment already expired.
7284
        $generator->enrol_user($s3->id, $c1->id, 'student', 'manual', 0, $now - YEARSECS);
7285
        $this->assertEquals(3, average_number_of_participants());
7286
        $this->assertEquals(2, average_number_of_participants(true));
7287
 
7288
        // Student 4 enrolled in both courses, but the enrolment has been suspended.
7289
        $generator->enrol_user($s4->id, $c1->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
7290
        $generator->enrol_user($s4->id, $c2->id, 'student', 'manual', $now - DAYSECS, $now + YEARSECS, ENROL_USER_SUSPENDED);
7291
        $this->assertEquals(4, average_number_of_participants());
7292
        $this->assertEquals(2, average_number_of_participants(true));
7293
 
7294
        // Consider only t1 and s1 who logged in recently.
7295
        $this->assertEquals(1.5, average_number_of_participants(false, $now - DAYSECS));
7296
 
7297
        // Consider only t1, s1, s2 and s3 who logged in in recent weeks.
7298
        $this->assertEquals(3, average_number_of_participants(false, $now - 4 * WEEKSECS));
7299
 
7300
        // Hidden courses are excluded from stats.
7301
        $DB->set_field('course', 'visible', 0, ['id' => $c1->id]);
7302
        $this->assertEquals(3, average_number_of_participants());
7303
        $this->assertEquals(1, average_number_of_participants(true));
7304
    }
7305
 
7306
    /**
7307
     * Test the set_downloadcontent() function.
7308
     */
7309
    public function test_set_downloadcontent() {
7310
        $this->resetAfterTest();
7311
 
7312
        $generator = $this->getDataGenerator();
7313
        $course = $generator->create_course();
7314
        $page = $generator->create_module('page', ['course' => $course]);
7315
 
7316
        // Test the module 'downloadcontent' field is set to enabled.
7317
        set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_ENABLED);
7318
        $modinfo = get_fast_modinfo($course)->get_cm($page->cmid);
7319
        $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $modinfo->downloadcontent);
7320
 
7321
        // Now let's test the 'downloadcontent' value is updated to disabled.
7322
        set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_DISABLED);
7323
        $modinfo = get_fast_modinfo($course)->get_cm($page->cmid);
7324
        $this->assertEquals(DOWNLOAD_COURSE_CONTENT_DISABLED, $modinfo->downloadcontent);
7325
 
7326
        // Nothing to update, the download course content value is the same, it should return false.
7327
        $this->assertFalse(set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_DISABLED));
7328
 
7329
        // The download course content value has changed, it should return true in this case.
7330
        $this->assertTrue(set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_ENABLED));
7331
    }
7332
 
7333
    /**
7334
     * Test for course_get_courseimage.
7335
     *
7336
     * @covers ::course_get_courseimage
7337
     */
7338
    public function test_course_get_courseimage(): void {
7339
        global $CFG;
7340
 
7341
        $this->resetAfterTest();
7342
        $generator = $this->getDataGenerator();
7343
        $course = $generator->create_course();
7344
 
7345
        $this->assertNull(course_get_courseimage($course));
7346
 
7347
        $fs = get_file_storage();
7348
        $file = $fs->create_file_from_pathname((object) [
7349
            'contextid' => \core\context\course::instance($course->id)->id,
7350
            'component' => 'course',
7351
            'filearea' => 'overviewfiles',
7352
            'itemid' => 0,
7353
            'filepath' => '/',
7354
            'filename' => 'logo.png',
7355
        ], "{$CFG->dirroot}/lib/tests/fixtures/gd-logo.png");
7356
 
7357
        $image = course_get_courseimage($course);
7358
        $this->assertInstanceOf(\stored_file::class, $image);
7359
        $this->assertEquals(
7360
            $file->get_id(),
7361
            $image->get_id(),
7362
        );
7363
    }
7364
 
7365
    /**
7366
     * Test the course_get_communication_instance_data() function.
7367
     *
7368
     * @covers ::course_get_communication_instance_data
7369
     */
7370
    public function test_course_get_communication_instance_data(): void {
7371
        $this->resetAfterTest();
7372
        $course = $this->getDataGenerator()->create_course();
7373
 
7374
        // Set admin user as a valid enrolment will be checked in the callback function.
7375
        $this->setAdminUser();
7376
 
7377
        // Use the callback function and return the data.
7378
        list($instance, $context, $heading, $returnurl) = component_callback(
7379
            'core_course',
7380
            'get_communication_instance_data',
7381
            [$course->id]
7382
        );
7383
 
7384
        // Check the url is as expected.
7385
        $expectedreturnurl = new moodle_url('/course/view.php', ['id' => $course->id]);
7386
        $this->assertEquals($expectedreturnurl, $returnurl);
7387
 
7388
        // Check the context is as expected.
7389
        $expectedcontext = context_course::instance($course->id);
7390
        $this->assertEquals($expectedcontext, $context);
7391
 
7392
        // Check the instance id is as expected.
7393
        $this->assertEquals($course->id, $instance->id);
7394
 
7395
        // Check the heading is as expected.
7396
        $this->assertEquals($course->fullname, $heading);
7397
    }
7398
 
7399
    /**
7400
     * Test course_section_view() function
7401
     *
7402
     * @covers ::course_section_view
7403
     */
7404
    public function test_course_section_view(): void {
7405
 
7406
        $this->resetAfterTest();
7407
 
7408
        // Course without sections.
7409
        $course = $this->getDataGenerator()->create_course(['numsections' => 5], ['createsections' => true]);
7410
        $coursecontext = context_course::instance($course->id);
7411
        $format = course_get_format($course->id);
7412
        $sections = $format->get_sections();
7413
        $section = reset($sections);
7414
 
7415
        // Redirect events to the sink, so we can recover them later.
7416
        $sink = $this->redirectEvents();
7417
 
7418
        course_section_view($coursecontext, $section->id);
7419
 
7420
        $events = $sink->get_events();
7421
        $event = reset($events);
7422
 
7423
        // Check the event details are correct.
7424
        $this->assertInstanceOf('\core\event\section_viewed', $event);
7425
        $this->assertEquals(context_course::instance($course->id), $event->get_context());
7426
        $this->assertEquals('course_sections', $event->objecttable);
7427
        $this->assertEquals($section->id, $event->objectid);
7428
    }
7429
}