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