MDL-22078 course: End date tests
[moodle.git] / backup / moodle2 / tests / moodle2_test.php
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/>.
17 /**
18  * Tests for Moodle 2 format backup operation.
19  *
20  * @package core_backup
21  * @copyright 2014 The Open University
22  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 global $CFG;
28 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
29 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
30 require_once($CFG->libdir . '/completionlib.php');
32 /**
33  * Tests for Moodle 2 format backup operation.
34  *
35  * @package core_backup
36  * @copyright 2014 The Open University
37  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class core_backup_moodle2_testcase extends advanced_testcase {
41     /**
42      * Tests the availability field on modules and sections is correctly
43      * backed up and restored.
44      */
45     public function test_backup_availability() {
46         global $DB, $CFG;
48         $this->resetAfterTest(true);
49         $this->setAdminUser();
50         $CFG->enableavailability = true;
51         $CFG->enablecompletion = true;
53         // Create a course with some availability data set.
54         $generator = $this->getDataGenerator();
55         $course = $generator->create_course(
56                 array('format' => 'topics', 'numsections' => 3,
57                     'enablecompletion' => COMPLETION_ENABLED),
58                 array('createsections' => true));
59         $forum = $generator->create_module('forum', array(
60                 'course' => $course->id));
61         $forum2 = $generator->create_module('forum', array(
62                 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
64         // We need a grade, easiest is to add an assignment.
65         $assignrow = $generator->create_module('assign', array(
66                 'course' => $course->id));
67         $assign = new assign(context_module::instance($assignrow->cmid), false, false);
68         $item = $assign->get_grade_item();
70         // Make a test grouping as well.
71         $grouping = $generator->create_grouping(array('courseid' => $course->id,
72                 'name' => 'Grouping!'));
74         $availability = '{"op":"|","show":false,"c":[' .
75                 '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' .
76                 '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' .
77                 '{"type":"grouping","id":' . $grouping->id . '}' .
78                 ']}';
79         $DB->set_field('course_modules', 'availability', $availability, array(
80                 'id' => $forum->cmid));
81         $DB->set_field('course_sections', 'availability', $availability, array(
82                 'course' => $course->id, 'section' => 1));
84         // Backup and restore it.
85         $newcourseid = $this->backup_and_restore($course);
87         // Check settings in new course.
88         $modinfo = get_fast_modinfo($newcourseid);
89         $forums = array_values($modinfo->get_instances_of('forum'));
90         $assigns = array_values($modinfo->get_instances_of('assign'));
91         $newassign = new assign(context_module::instance($assigns[0]->id), false, false);
92         $newitem = $newassign->get_grade_item();
93         $newgroupingid = $DB->get_field('groupings', 'id', array('courseid' => $newcourseid));
95         // Expected availability should have new ID for the forum, grade, and grouping.
96         $newavailability = str_replace(
97                 '"grouping","id":' . $grouping->id,
98                 '"grouping","id":' . $newgroupingid,
99                 str_replace(
100                     '"grade","id":' . $item->id,
101                     '"grade","id":' . $newitem->id,
102                     str_replace(
103                         '"cm":' . $forum2->cmid,
104                         '"cm":' . $forums[1]->id,
105                         $availability)));
107         $this->assertEquals($newavailability, $forums[0]->availability);
108         $this->assertNull($forums[1]->availability);
109         $this->assertEquals($newavailability, $modinfo->get_section_info(1, MUST_EXIST)->availability);
110         $this->assertNull($modinfo->get_section_info(2, MUST_EXIST)->availability);
111     }
113     /**
114      * The availability data format was changed in Moodle 2.7. This test
115      * ensures that a Moodle 2.6 backup with this data can still be correctly
116      * restored.
117      */
118     public function test_restore_legacy_availability() {
119         global $DB, $USER, $CFG;
120         require_once($CFG->dirroot . '/grade/querylib.php');
121         require_once($CFG->libdir . '/completionlib.php');
123         $this->resetAfterTest(true);
124         $this->setAdminUser();
125         $CFG->enableavailability = true;
126         $CFG->enablecompletion = true;
128         // Extract backup file.
129         $backupid = 'abc';
130         $backuppath = $CFG->tempdir . '/backup/' . $backupid;
131         check_dir_exists($backuppath);
132         get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
133                 __DIR__ . '/fixtures/availability_26_format.mbz', $backuppath);
135         // Do restore to new course with default settings.
136         $generator = $this->getDataGenerator();
137         $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
138         $newcourseid = restore_dbops::create_new_course(
139                 'Test fullname', 'Test shortname', $categoryid);
140         $rc = new restore_controller($backupid, $newcourseid,
141                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
142                 backup::TARGET_NEW_COURSE);
143         $thrown = null;
144         try {
145             $this->assertTrue($rc->execute_precheck());
146             $rc->execute_plan();
147             $rc->destroy();
148         } catch (Exception $e) {
149             $thrown = $e;
150             // Because of the PHPUnit exception behaviour in this situation, we
151             // will not see this message unless it is explicitly echoed (just
152             // using it in a fail() call or similar will not work).
153             echo "\n\nEXCEPTION: " . $thrown->getMessage() . '[' .
154                     $thrown->getFile() . ':' . $thrown->getLine(). "]\n\n";
155         }
157         $this->assertNull($thrown);
159         // Get information about the resulting course and check that it is set
160         // up correctly.
161         $modinfo = get_fast_modinfo($newcourseid);
162         $pages = array_values($modinfo->get_instances_of('page'));
163         $forums = array_values($modinfo->get_instances_of('forum'));
164         $quizzes = array_values($modinfo->get_instances_of('quiz'));
165         $grouping = $DB->get_record('groupings', array('courseid' => $newcourseid));
167         // FROM date.
168         $this->assertEquals(
169                 '{"op":"&","showc":[true],"c":[{"type":"date","d":">=","t":1893456000}]}',
170                 $pages[1]->availability);
171         // UNTIL date.
172         $this->assertEquals(
173                 '{"op":"&","showc":[false],"c":[{"type":"date","d":"<","t":1393977600}]}',
174                 $pages[2]->availability);
175         // FROM and UNTIL.
176         $this->assertEquals(
177                 '{"op":"&","showc":[true,false],"c":[' .
178                 '{"type":"date","d":">=","t":1449705600},' .
179                 '{"type":"date","d":"<","t":1893456000}' .
180                 ']}',
181                 $pages[3]->availability);
182         // Grade >= 75%.
183         $grades = array_values(grade_get_grade_items_for_activity($quizzes[0], true));
184         $gradeid = $grades[0]->id;
185         $coursegrade = grade_item::fetch_course_item($newcourseid);
186         $this->assertEquals(
187                 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":75}]}',
188                 $pages[4]->availability);
189         // Grade < 25%.
190         $this->assertEquals(
191                 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"max":25}]}',
192                 $pages[5]->availability);
193         // Grade 90-100%.
194         $this->assertEquals(
195                 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":90,"max":100}]}',
196                 $pages[6]->availability);
197         // Email contains frog.
198         $this->assertEquals(
199                 '{"op":"&","showc":[true],"c":[{"type":"profile","op":"contains","sf":"email","v":"frog"}]}',
200                 $pages[7]->availability);
201         // Page marked complete..
202         $this->assertEquals(
203                 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $pages[0]->id .
204                 ',"e":' . COMPLETION_COMPLETE . '}]}',
205                 $pages[8]->availability);
206         // Quiz complete but failed.
207         $this->assertEquals(
208                 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id .
209                 ',"e":' . COMPLETION_COMPLETE_FAIL . '}]}',
210                 $pages[9]->availability);
211         // Quiz complete and succeeded.
212         $this->assertEquals(
213                 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id .
214                 ',"e":' . COMPLETION_COMPLETE_PASS. '}]}',
215                 $pages[10]->availability);
216         // Quiz not complete.
217         $this->assertEquals(
218                 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id .
219                 ',"e":' . COMPLETION_INCOMPLETE . '}]}',
220                 $pages[11]->availability);
221         // Grouping.
222         $this->assertEquals(
223                 '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}',
224                 $pages[12]->availability);
226         // All the options.
227         $this->assertEquals('{"op":"&",' .
228                 '"showc":[false,true,false,true,true,true,true,true,true],' .
229                 '"c":[' .
230                 '{"type":"grouping","id":' . $grouping->id . '},' .
231                 '{"type":"date","d":">=","t":1488585600},' .
232                 '{"type":"date","d":"<","t":1709510400},' .
233                 '{"type":"profile","op":"contains","sf":"email","v":"@"},' .
234                 '{"type":"profile","op":"contains","sf":"city","v":"Frogtown"},' .
235                 '{"type":"grade","id":' . $gradeid . ',"min":30,"max":35},' .
236                 '{"type":"grade","id":' . $coursegrade->id . ',"min":5,"max":10},' .
237                 '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '},' .
238                 '{"type":"completion","cm":' . $quizzes[0]->id .',"e":' . COMPLETION_INCOMPLETE . '}' .
239                 ']}', $pages[13]->availability);
241         // Group members only forum.
242         $this->assertEquals(
243                 '{"op":"&","showc":[false],"c":[{"type":"group"}]}',
244                 $forums[0]->availability);
246         // Section with lots of conditions.
247         $this->assertEquals(
248                 '{"op":"&","showc":[false,false,false,false],"c":[' .
249                 '{"type":"date","d":">=","t":1417737600},' .
250                 '{"type":"profile","op":"contains","sf":"email","v":"@"},' .
251                 '{"type":"grade","id":' . $gradeid . ',"min":20},' .
252                 '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '}]}',
253                 $modinfo->get_section_info(3)->availability);
255         // Section with grouping.
256         $this->assertEquals(
257                 '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}',
258                 $modinfo->get_section_info(4)->availability);
259     }
261     /**
262      * Tests the backup and restore of single activity to same course (duplicate)
263      * when it contains availability conditions that depend on other items in
264      * course.
265      */
266     public function test_duplicate_availability() {
267         global $DB, $CFG;
269         $this->resetAfterTest(true);
270         $this->setAdminUser();
271         $CFG->enableavailability = true;
272         $CFG->enablecompletion = true;
274         // Create a course with completion enabled and 2 forums.
275         $generator = $this->getDataGenerator();
276         $course = $generator->create_course(
277                 array('format' => 'topics', 'enablecompletion' => COMPLETION_ENABLED));
278         $forum = $generator->create_module('forum', array(
279                 'course' => $course->id));
280         $forum2 = $generator->create_module('forum', array(
281                 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
283         // We need a grade, easiest is to add an assignment.
284         $assignrow = $generator->create_module('assign', array(
285                 'course' => $course->id));
286         $assign = new assign(context_module::instance($assignrow->cmid), false, false);
287         $item = $assign->get_grade_item();
289         // Make a test group and grouping as well.
290         $group = $generator->create_group(array('courseid' => $course->id,
291                 'name' => 'Group!'));
292         $grouping = $generator->create_grouping(array('courseid' => $course->id,
293                 'name' => 'Grouping!'));
295         // Set the forum to have availability conditions on all those things,
296         // plus some that don't exist or are special values.
297         $availability = '{"op":"|","show":false,"c":[' .
298                 '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' .
299                 '{"type":"completion","cm":99999999,"e":1},' .
300                 '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' .
301                 '{"type":"grade","id":99999998,"min":4,"max":94},' .
302                 '{"type":"grouping","id":' . $grouping->id . '},' .
303                 '{"type":"grouping","id":99999997},' .
304                 '{"type":"group","id":' . $group->id . '},' .
305                 '{"type":"group"},' .
306                 '{"type":"group","id":99999996}' .
307                 ']}';
308         $DB->set_field('course_modules', 'availability', $availability, array(
309                 'id' => $forum->cmid));
311         // Duplicate it.
312         $newcmid = $this->duplicate($course, $forum->cmid);
314         // For those which still exist on the course we expect it to keep using
315         // the real ID. For those which do not exist on the course any more
316         // (e.g. simulating backup/restore of single activity between 2 courses)
317         // we expect the IDs to be replaced with marker value: 0 for cmid
318         // and grade, -1 for group/grouping.
319         $expected = str_replace(
320                 array('99999999', '99999998', '99999997', '99999996'),
321                 array(0, 0, -1, -1),
322                 $availability);
324         // Check settings in new activity.
325         $actual = $DB->get_field('course_modules', 'availability', array('id' => $newcmid));
326         $this->assertEquals($expected, $actual);
327     }
329     /**
330      * When restoring a course, you can change the start date, which shifts other
331      * dates. This test checks that certain dates are correctly modified.
332      */
333     public function test_restore_dates() {
334         global $DB, $CFG;
336         $this->resetAfterTest(true);
337         $this->setAdminUser();
338         $CFG->enableavailability = true;
340         // Create a course with specific start date.
341         $generator = $this->getDataGenerator();
342         $course = $generator->create_course(array(
343             'startdate' => strtotime('1 Jan 2014 00:00 GMT'),
344             'enddate' => strtotime('3 Aug 2014 00:00 GMT')
345         ));
347         // Add a forum with conditional availability date restriction, including
348         // one of them nested inside a tree.
349         $availability = '{"op":"&","showc":[true,true],"c":[' .
350                 '{"op":"&","c":[{"type":"date","d":">=","t":DATE1}]},' .
351                 '{"type":"date","d":"<","t":DATE2}]}';
352         $before = str_replace(
353                 array('DATE1', 'DATE2'),
354                 array(strtotime('1 Feb 2014 00:00 GMT'), strtotime('10 Feb 2014 00:00 GMT')),
355                 $availability);
356         $forum = $generator->create_module('forum', array('course' => $course->id,
357                 'availability' => $before));
359         // Add an assign with defined start date.
360         $assign = $generator->create_module('assign', array('course' => $course->id,
361                 'allowsubmissionsfromdate' => strtotime('7 Jan 2014 16:00 GMT')));
363         // Do backup and restore.
364         $newcourseid = $this->backup_and_restore($course, strtotime('3 Jan 2015 00:00 GMT'));
366         $newcourse = $DB->get_record('course', array('id' => $newcourseid));
367         $this->assertEquals(strtotime('5 Aug 2015 00:00 GMT'), $newcourse->enddate);
369         $modinfo = get_fast_modinfo($newcourseid);
371         // Check forum dates are modified by the same amount as the course start.
372         $newforums = $modinfo->get_instances_of('forum');
373         $newforum = reset($newforums);
374         $after = str_replace(
375             array('DATE1', 'DATE2'),
376             array(strtotime('3 Feb 2015 00:00 GMT'), strtotime('12 Feb 2015 00:00 GMT')),
377             $availability);
378         $this->assertEquals($after, $newforum->availability);
380         // Check assign date.
381         $newassigns = $modinfo->get_instances_of('assign');
382         $newassign = reset($newassigns);
383         $this->assertEquals(strtotime('9 Jan 2015 16:00 GMT'), $DB->get_field(
384                 'assign', 'allowsubmissionsfromdate', array('id' => $newassign->instance)));
385     }
387     /**
388      * Test front page backup/restore and duplicate activities
389      * @return void
390      */
391     public function test_restore_frontpage() {
392         global $DB, $CFG, $USER;
394         $this->resetAfterTest(true);
395         $this->setAdminUser();
396         $generator = $this->getDataGenerator();
398         $frontpage = $DB->get_record('course', array('id' => SITEID));
399         $forum = $generator->create_module('forum', array('course' => $frontpage->id));
401         // Activities can be duplicated.
402         $this->duplicate($frontpage, $forum->cmid);
404         $modinfo = get_fast_modinfo($frontpage);
405         $this->assertEquals(2, count($modinfo->get_instances_of('forum')));
407         // Front page backup.
408         $frontpagebc = new backup_controller(backup::TYPE_1COURSE, $frontpage->id,
409                 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
410                 $USER->id);
411         $frontpagebackupid = $frontpagebc->get_backupid();
412         $frontpagebc->execute_plan();
413         $frontpagebc->destroy();
415         $course = $generator->create_course();
416         $newcourseid = restore_dbops::create_new_course(
417                 $course->fullname . ' 2', $course->shortname . '_2', $course->category);
419         // Other course backup.
420         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
421                 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
422                 $USER->id);
423         $otherbackupid = $bc->get_backupid();
424         $bc->execute_plan();
425         $bc->destroy();
427         // We can only restore a front page over the front page.
428         $rc = new restore_controller($frontpagebackupid, $course->id,
429                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
430                 backup::TARGET_CURRENT_ADDING);
431         $this->assertFalse($rc->execute_precheck());
432         $rc->destroy();
434         $rc = new restore_controller($frontpagebackupid, $newcourseid,
435                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
436                 backup::TARGET_NEW_COURSE);
437         $this->assertFalse($rc->execute_precheck());
438         $rc->destroy();
440         $rc = new restore_controller($frontpagebackupid, $frontpage->id,
441                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
442                 backup::TARGET_CURRENT_ADDING);
443         $this->assertTrue($rc->execute_precheck());
444         $rc->execute_plan();
445         $rc->destroy();
447         // We can't restore a non-front page course on the front page course.
448         $rc = new restore_controller($otherbackupid, $frontpage->id,
449                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
450                 backup::TARGET_CURRENT_ADDING);
451         $this->assertFalse($rc->execute_precheck());
452         $rc->destroy();
454         $rc = new restore_controller($otherbackupid, $newcourseid,
455                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
456                 backup::TARGET_NEW_COURSE);
457         $this->assertTrue($rc->execute_precheck());
458         $rc->execute_plan();
459         $rc->destroy();
460     }
462     /**
463      * Backs a course up and restores it.
464      *
465      * @param stdClass $course Course object to backup
466      * @param int $newdate If non-zero, specifies custom date for new course
467      * @return int ID of newly restored course
468      */
469     protected function backup_and_restore($course, $newdate = 0) {
470         global $USER, $CFG;
472         // Turn off file logging, otherwise it can't delete the file (Windows).
473         $CFG->backup_file_logger_level = backup::LOG_NONE;
475         // Do backup with default settings. MODE_IMPORT means it will just
476         // create the directory and not zip it.
477         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
478                 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
479                 $USER->id);
480         $backupid = $bc->get_backupid();
481         $bc->execute_plan();
482         $bc->destroy();
484         // Do restore to new course with default settings.
485         $newcourseid = restore_dbops::create_new_course(
486                 $course->fullname, $course->shortname . '_2', $course->category);
487         $rc = new restore_controller($backupid, $newcourseid,
488                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
489                 backup::TARGET_NEW_COURSE);
490         if ($newdate) {
491             $rc->get_plan()->get_setting('course_startdate')->set_value($newdate);
492         }
493         $this->assertTrue($rc->execute_precheck());
494         $rc->execute_plan();
495         $rc->destroy();
497         return $newcourseid;
498     }
500     /**
501      * Duplicates a single activity within a course.
502      *
503      * This is based on the code from course/modduplicate.php, but reduced for
504      * simplicity.
505      *
506      * @param stdClass $course Course object
507      * @param int $cmid Activity to duplicate
508      * @return int ID of new activity
509      */
510     protected function duplicate($course, $cmid) {
511         global $USER;
513         // Do backup.
514         $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cmid, backup::FORMAT_MOODLE,
515                 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
516         $backupid = $bc->get_backupid();
517         $bc->execute_plan();
518         $bc->destroy();
520         // Do restore.
521         $rc = new restore_controller($backupid, $course->id,
522                 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
523         $this->assertTrue($rc->execute_precheck());
524         $rc->execute_plan();
526         // Find cmid.
527         $tasks = $rc->get_plan()->get_tasks();
528         $cmcontext = context_module::instance($cmid);
529         $newcmid = 0;
530         foreach ($tasks as $task) {
531             if (is_subclass_of($task, 'restore_activity_task')) {
532                 if ($task->get_old_contextid() == $cmcontext->id) {
533                     $newcmid = $task->get_moduleid();
534                     break;
535                 }
536             }
537         }
538         $rc->destroy();
539         if (!$newcmid) {
540             throw new coding_exception('Unexpected: failure to find restored cmid');
541         }
542         return $newcmid;
543     }