Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace core_backup; 18 19 use backup; 20 use backup_controller; 21 use backup_setting; 22 use restore_controller; 23 use restore_dbops; 24 25 defined('MOODLE_INTERNAL') || die(); 26 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'); 31 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 moodle2_test extends \advanced_testcase { 40 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; 47 48 $this->resetAfterTest(true); 49 $this->setAdminUser(); 50 $CFG->enableavailability = true; 51 $CFG->enablecompletion = true; 52 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)); 63 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(); 69 70 // Make a test grouping as well. 71 $grouping = $generator->create_grouping(array('courseid' => $course->id, 72 'name' => 'Grouping!')); 73 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)); 83 84 // Backup and restore it. 85 $newcourseid = $this->backup_and_restore($course); 86 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)); 94 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))); 106 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 } 112 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'); 122 123 $this->resetAfterTest(true); 124 $this->setAdminUser(); 125 $CFG->enableavailability = true; 126 $CFG->enablecompletion = true; 127 128 // Extract backup file. 129 $backupid = 'abc'; 130 $backuppath = make_backup_temp_directory($backupid); 131 get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( 132 __DIR__ . '/fixtures/availability_26_format.mbz', $backuppath); 133 134 // Do restore to new course with default settings. 135 $generator = $this->getDataGenerator(); 136 $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); 137 $newcourseid = restore_dbops::create_new_course( 138 'Test fullname', 'Test shortname', $categoryid); 139 $rc = new restore_controller($backupid, $newcourseid, 140 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 141 backup::TARGET_NEW_COURSE); 142 $thrown = null; 143 try { 144 $this->assertTrue($rc->execute_precheck()); 145 $rc->execute_plan(); 146 $rc->destroy(); 147 } catch (Exception $e) { 148 $thrown = $e; 149 // Because of the PHPUnit exception behaviour in this situation, we 150 // will not see this message unless it is explicitly echoed (just 151 // using it in a fail() call or similar will not work). 152 echo "\n\nEXCEPTION: " . $thrown->getMessage() . '[' . 153 $thrown->getFile() . ':' . $thrown->getLine(). "]\n\n"; 154 } 155 156 $this->assertNull($thrown); 157 158 // Get information about the resulting course and check that it is set 159 // up correctly. 160 $modinfo = get_fast_modinfo($newcourseid); 161 $pages = array_values($modinfo->get_instances_of('page')); 162 $forums = array_values($modinfo->get_instances_of('forum')); 163 $quizzes = array_values($modinfo->get_instances_of('quiz')); 164 $grouping = $DB->get_record('groupings', array('courseid' => $newcourseid)); 165 166 // FROM date. 167 $this->assertEquals( 168 '{"op":"&","showc":[true],"c":[{"type":"date","d":">=","t":1893456000}]}', 169 $pages[1]->availability); 170 // UNTIL date. 171 $this->assertEquals( 172 '{"op":"&","showc":[false],"c":[{"type":"date","d":"<","t":1393977600}]}', 173 $pages[2]->availability); 174 // FROM and UNTIL. 175 $this->assertEquals( 176 '{"op":"&","showc":[true,false],"c":[' . 177 '{"type":"date","d":">=","t":1449705600},' . 178 '{"type":"date","d":"<","t":1893456000}' . 179 ']}', 180 $pages[3]->availability); 181 // Grade >= 75%. 182 $grades = array_values(grade_get_grade_items_for_activity($quizzes[0], true)); 183 $gradeid = $grades[0]->id; 184 $coursegrade = \grade_item::fetch_course_item($newcourseid); 185 $this->assertEquals( 186 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":75}]}', 187 $pages[4]->availability); 188 // Grade < 25%. 189 $this->assertEquals( 190 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"max":25}]}', 191 $pages[5]->availability); 192 // Grade 90-100%. 193 $this->assertEquals( 194 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":90,"max":100}]}', 195 $pages[6]->availability); 196 // Email contains frog. 197 $this->assertEquals( 198 '{"op":"&","showc":[true],"c":[{"type":"profile","op":"contains","sf":"email","v":"frog"}]}', 199 $pages[7]->availability); 200 // Page marked complete.. 201 $this->assertEquals( 202 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $pages[0]->id . 203 ',"e":' . COMPLETION_COMPLETE . '}]}', 204 $pages[8]->availability); 205 // Quiz complete but failed. 206 $this->assertEquals( 207 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id . 208 ',"e":' . COMPLETION_COMPLETE_FAIL . '}]}', 209 $pages[9]->availability); 210 // Quiz complete and succeeded. 211 $this->assertEquals( 212 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id . 213 ',"e":' . COMPLETION_COMPLETE_PASS. '}]}', 214 $pages[10]->availability); 215 // Quiz not complete. 216 $this->assertEquals( 217 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id . 218 ',"e":' . COMPLETION_INCOMPLETE . '}]}', 219 $pages[11]->availability); 220 // Grouping. 221 $this->assertEquals( 222 '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}', 223 $pages[12]->availability); 224 225 // All the options. 226 $this->assertEquals('{"op":"&",' . 227 '"showc":[false,true,false,true,true,true,true,true,true],' . 228 '"c":[' . 229 '{"type":"grouping","id":' . $grouping->id . '},' . 230 '{"type":"date","d":">=","t":1488585600},' . 231 '{"type":"date","d":"<","t":1709510400},' . 232 '{"type":"profile","op":"contains","sf":"email","v":"@"},' . 233 '{"type":"profile","op":"contains","sf":"city","v":"Frogtown"},' . 234 '{"type":"grade","id":' . $gradeid . ',"min":30,"max":35},' . 235 '{"type":"grade","id":' . $coursegrade->id . ',"min":5,"max":10},' . 236 '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '},' . 237 '{"type":"completion","cm":' . $quizzes[0]->id .',"e":' . COMPLETION_INCOMPLETE . '}' . 238 ']}', $pages[13]->availability); 239 240 // Group members only forum. 241 $this->assertEquals( 242 '{"op":"&","showc":[false],"c":[{"type":"group"}]}', 243 $forums[0]->availability); 244 245 // Section with lots of conditions. 246 $this->assertEquals( 247 '{"op":"&","showc":[false,false,false,false],"c":[' . 248 '{"type":"date","d":">=","t":1417737600},' . 249 '{"type":"profile","op":"contains","sf":"email","v":"@"},' . 250 '{"type":"grade","id":' . $gradeid . ',"min":20},' . 251 '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '}]}', 252 $modinfo->get_section_info(3)->availability); 253 254 // Section with grouping. 255 $this->assertEquals( 256 '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}', 257 $modinfo->get_section_info(4)->availability); 258 } 259 260 /** 261 * Tests the backup and restore of single activity to same course (duplicate) 262 * when it contains availability conditions that depend on other items in 263 * course. 264 */ 265 public function test_duplicate_availability() { 266 global $DB, $CFG; 267 268 $this->resetAfterTest(true); 269 $this->setAdminUser(); 270 $CFG->enableavailability = true; 271 $CFG->enablecompletion = true; 272 273 // Create a course with completion enabled and 2 forums. 274 $generator = $this->getDataGenerator(); 275 $course = $generator->create_course( 276 array('format' => 'topics', 'enablecompletion' => COMPLETION_ENABLED)); 277 $forum = $generator->create_module('forum', array( 278 'course' => $course->id)); 279 $forum2 = $generator->create_module('forum', array( 280 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL)); 281 282 // We need a grade, easiest is to add an assignment. 283 $assignrow = $generator->create_module('assign', array( 284 'course' => $course->id)); 285 $assign = new \assign(\context_module::instance($assignrow->cmid), false, false); 286 $item = $assign->get_grade_item(); 287 288 // Make a test group and grouping as well. 289 $group = $generator->create_group(array('courseid' => $course->id, 290 'name' => 'Group!')); 291 $grouping = $generator->create_grouping(array('courseid' => $course->id, 292 'name' => 'Grouping!')); 293 294 // Set the forum to have availability conditions on all those things, 295 // plus some that don't exist or are special values. 296 $availability = '{"op":"|","show":false,"c":[' . 297 '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' . 298 '{"type":"completion","cm":99999999,"e":1},' . 299 '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' . 300 '{"type":"grade","id":99999998,"min":4,"max":94},' . 301 '{"type":"grouping","id":' . $grouping->id . '},' . 302 '{"type":"grouping","id":99999997},' . 303 '{"type":"group","id":' . $group->id . '},' . 304 '{"type":"group"},' . 305 '{"type":"group","id":99999996}' . 306 ']}'; 307 $DB->set_field('course_modules', 'availability', $availability, array( 308 'id' => $forum->cmid)); 309 310 // Duplicate it. 311 $newcmid = $this->duplicate($course, $forum->cmid); 312 313 // For those which still exist on the course we expect it to keep using 314 // the real ID. For those which do not exist on the course any more 315 // (e.g. simulating backup/restore of single activity between 2 courses) 316 // we expect the IDs to be replaced with marker value: 0 for cmid 317 // and grade, -1 for group/grouping. 318 $expected = str_replace( 319 array('99999999', '99999998', '99999997', '99999996'), 320 array(0, 0, -1, -1), 321 $availability); 322 323 // Check settings in new activity. 324 $actual = $DB->get_field('course_modules', 'availability', array('id' => $newcmid)); 325 $this->assertEquals($expected, $actual); 326 } 327 328 /** 329 * When restoring a course, you can change the start date, which shifts other 330 * dates. This test checks that certain dates are correctly modified. 331 */ 332 public function test_restore_dates() { 333 global $DB, $CFG; 334 335 $this->resetAfterTest(true); 336 $this->setAdminUser(); 337 $CFG->enableavailability = true; 338 339 // Create a course with specific start date. 340 $generator = $this->getDataGenerator(); 341 $course = $generator->create_course(array( 342 'startdate' => strtotime('1 Jan 2014 00:00 GMT'), 343 'enddate' => strtotime('3 Aug 2014 00:00 GMT') 344 )); 345 346 // Add a forum with conditional availability date restriction, including 347 // one of them nested inside a tree. 348 $availability = '{"op":"&","showc":[true,true],"c":[' . 349 '{"op":"&","c":[{"type":"date","d":">=","t":DATE1}]},' . 350 '{"type":"date","d":"<","t":DATE2}]}'; 351 $before = str_replace( 352 array('DATE1', 'DATE2'), 353 array(strtotime('1 Feb 2014 00:00 GMT'), strtotime('10 Feb 2014 00:00 GMT')), 354 $availability); 355 $forum = $generator->create_module('forum', array('course' => $course->id, 356 'availability' => $before)); 357 358 // Add an assign with defined start date. 359 $assign = $generator->create_module('assign', array('course' => $course->id, 360 'allowsubmissionsfromdate' => strtotime('7 Jan 2014 16:00 GMT'))); 361 362 // Do backup and restore. 363 $newcourseid = $this->backup_and_restore($course, strtotime('3 Jan 2015 00:00 GMT')); 364 365 $newcourse = $DB->get_record('course', array('id' => $newcourseid)); 366 $this->assertEquals(strtotime('5 Aug 2015 00:00 GMT'), $newcourse->enddate); 367 368 $modinfo = get_fast_modinfo($newcourseid); 369 370 // Check forum dates are modified by the same amount as the course start. 371 $newforums = $modinfo->get_instances_of('forum'); 372 $newforum = reset($newforums); 373 $after = str_replace( 374 array('DATE1', 'DATE2'), 375 array(strtotime('3 Feb 2015 00:00 GMT'), strtotime('12 Feb 2015 00:00 GMT')), 376 $availability); 377 $this->assertEquals($after, $newforum->availability); 378 379 // Check assign date. 380 $newassigns = $modinfo->get_instances_of('assign'); 381 $newassign = reset($newassigns); 382 $this->assertEquals(strtotime('9 Jan 2015 16:00 GMT'), $DB->get_field( 383 'assign', 'allowsubmissionsfromdate', array('id' => $newassign->instance))); 384 } 385 386 /** 387 * Test front page backup/restore and duplicate activities 388 * @return void 389 */ 390 public function test_restore_frontpage() { 391 global $DB, $CFG, $USER; 392 393 $this->resetAfterTest(true); 394 $this->setAdminUser(); 395 $generator = $this->getDataGenerator(); 396 397 $frontpage = $DB->get_record('course', array('id' => SITEID)); 398 $forum = $generator->create_module('forum', array('course' => $frontpage->id)); 399 400 // Activities can be duplicated. 401 $this->duplicate($frontpage, $forum->cmid); 402 403 $modinfo = get_fast_modinfo($frontpage); 404 $this->assertEquals(2, count($modinfo->get_instances_of('forum'))); 405 406 // Front page backup. 407 $frontpagebc = new backup_controller(backup::TYPE_1COURSE, $frontpage->id, 408 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, 409 $USER->id); 410 $frontpagebackupid = $frontpagebc->get_backupid(); 411 $frontpagebc->execute_plan(); 412 $frontpagebc->destroy(); 413 414 $course = $generator->create_course(); 415 $newcourseid = restore_dbops::create_new_course( 416 $course->fullname . ' 2', $course->shortname . '_2', $course->category); 417 418 // Other course backup. 419 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, 420 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, 421 $USER->id); 422 $otherbackupid = $bc->get_backupid(); 423 $bc->execute_plan(); 424 $bc->destroy(); 425 426 // We can only restore a front page over the front page. 427 $rc = new restore_controller($frontpagebackupid, $course->id, 428 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 429 backup::TARGET_CURRENT_ADDING); 430 $this->assertFalse($rc->execute_precheck()); 431 $rc->destroy(); 432 433 $rc = new restore_controller($frontpagebackupid, $newcourseid, 434 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 435 backup::TARGET_NEW_COURSE); 436 $this->assertFalse($rc->execute_precheck()); 437 $rc->destroy(); 438 439 $rc = new restore_controller($frontpagebackupid, $frontpage->id, 440 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 441 backup::TARGET_CURRENT_ADDING); 442 $this->assertTrue($rc->execute_precheck()); 443 $rc->execute_plan(); 444 $rc->destroy(); 445 446 // We can't restore a non-front page course on the front page course. 447 $rc = new restore_controller($otherbackupid, $frontpage->id, 448 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 449 backup::TARGET_CURRENT_ADDING); 450 $this->assertFalse($rc->execute_precheck()); 451 $rc->destroy(); 452 453 $rc = new restore_controller($otherbackupid, $newcourseid, 454 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 455 backup::TARGET_NEW_COURSE); 456 $this->assertTrue($rc->execute_precheck()); 457 $rc->execute_plan(); 458 $rc->destroy(); 459 } 460 461 /** 462 * Backs a course up and restores it. 463 * 464 * @param \stdClass $course Course object to backup 465 * @param int $newdate If non-zero, specifies custom date for new course 466 * @param callable|null $inbetween If specified, function that is called before restore 467 * @param bool $userdata Whether the backup/restory must be with user data or not. 468 * @return int ID of newly restored course 469 */ 470 protected function backup_and_restore($course, $newdate = 0, $inbetween = null, bool $userdata = false) { 471 global $USER, $CFG; 472 473 // Turn off file logging, otherwise it can't delete the file (Windows). 474 $CFG->backup_file_logger_level = backup::LOG_NONE; 475 476 // Do backup with default settings. MODE_IMPORT means it will just 477 // create the directory and not zip it. 478 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, 479 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, 480 $USER->id); 481 $bc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED); 482 $bc->get_plan()->get_setting('users')->set_value($userdata); 483 484 $backupid = $bc->get_backupid(); 485 $bc->execute_plan(); 486 $bc->destroy(); 487 488 if ($inbetween) { 489 $inbetween($backupid); 490 } 491 492 // Do restore to new course with default settings. 493 $newcourseid = restore_dbops::create_new_course( 494 $course->fullname, $course->shortname . '_2', $course->category); 495 $rc = new restore_controller($backupid, $newcourseid, 496 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 497 backup::TARGET_NEW_COURSE); 498 if ($newdate) { 499 $rc->get_plan()->get_setting('course_startdate')->set_value($newdate); 500 } 501 502 $rc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED); 503 $rc->get_plan()->get_setting('users')->set_value($userdata); 504 if ($userdata) { 505 $rc->get_plan()->get_setting('xapistate')->set_value(true); 506 } 507 508 $this->assertTrue($rc->execute_precheck()); 509 $rc->execute_plan(); 510 $rc->destroy(); 511 512 return $newcourseid; 513 } 514 515 /** 516 * Duplicates a single activity within a course. 517 * 518 * This is based on the code from course/modduplicate.php, but reduced for 519 * simplicity. 520 * 521 * @param \stdClass $course Course object 522 * @param int $cmid Activity to duplicate 523 * @return int ID of new activity 524 */ 525 protected function duplicate($course, $cmid) { 526 global $USER; 527 528 // Do backup. 529 $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cmid, backup::FORMAT_MOODLE, 530 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); 531 $backupid = $bc->get_backupid(); 532 $bc->execute_plan(); 533 $bc->destroy(); 534 535 // Do restore. 536 $rc = new restore_controller($backupid, $course->id, 537 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING); 538 $this->assertTrue($rc->execute_precheck()); 539 $rc->execute_plan(); 540 541 // Find cmid. 542 $tasks = $rc->get_plan()->get_tasks(); 543 $cmcontext = \context_module::instance($cmid); 544 $newcmid = 0; 545 foreach ($tasks as $task) { 546 if (is_subclass_of($task, 'restore_activity_task')) { 547 if ($task->get_old_contextid() == $cmcontext->id) { 548 $newcmid = $task->get_moduleid(); 549 break; 550 } 551 } 552 } 553 $rc->destroy(); 554 if (!$newcmid) { 555 throw new \coding_exception('Unexpected: failure to find restored cmid'); 556 } 557 return $newcmid; 558 } 559 560 /** 561 * Help function for enrolment methods backup/restore tests: 562 * 563 * - Creates a course ($course), adds self-enrolment method and a user 564 * - Makes a backup 565 * - Creates a target course (if requested) ($newcourseid) 566 * - Initialises restore controller for this backup file ($rc) 567 * 568 * @param int $target target for restoring: backup::TARGET_NEW_COURSE etc. 569 * @param array $additionalcaps - additional capabilities to give to user 570 * @return array array of original course, new course id, restore controller: [$course, $newcourseid, $rc] 571 */ 572 protected function prepare_for_enrolments_test($target, $additionalcaps = []) { 573 global $CFG, $DB; 574 $this->resetAfterTest(true); 575 576 // Turn off file logging, otherwise it can't delete the file (Windows). 577 $CFG->backup_file_logger_level = backup::LOG_NONE; 578 579 $user = $this->getDataGenerator()->create_user(); 580 $roleidcat = create_role('Category role', 'dummyrole1', 'dummy role description'); 581 582 $course = $this->getDataGenerator()->create_course(); 583 584 // Enable instance of self-enrolment plugin (it should already be present) and enrol a student with it. 585 $selfplugin = enrol_get_plugin('self'); 586 $selfinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'self')); 587 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 588 $selfplugin->update_status($selfinstance, ENROL_INSTANCE_ENABLED); 589 $selfplugin->enrol_user($selfinstance, $user->id, $studentrole->id); 590 591 // Give current user capabilities to do backup and restore and assign student role. 592 $categorycontext = \context_course::instance($course->id)->get_parent_context(); 593 594 $caps = array_merge([ 595 'moodle/course:view', 596 'moodle/course:create', 597 'moodle/backup:backupcourse', 598 'moodle/backup:configure', 599 'moodle/backup:backuptargetimport', 600 'moodle/restore:restorecourse', 601 'moodle/role:assign', 602 'moodle/restore:configure', 603 ], $additionalcaps); 604 605 foreach ($caps as $cap) { 606 assign_capability($cap, CAP_ALLOW, $roleidcat, $categorycontext); 607 } 608 609 core_role_set_assign_allowed($roleidcat, $studentrole->id); 610 role_assign($roleidcat, $user->id, $categorycontext); 611 accesslib_clear_all_caches_for_unit_testing(); 612 613 $this->setUser($user); 614 615 // Do backup with default settings. MODE_IMPORT means it will just 616 // create the directory and not zip it. 617 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, 618 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_SAMESITE, 619 $user->id); 620 $backupid = $bc->get_backupid(); 621 $backupbasepath = $bc->get_plan()->get_basepath(); 622 $bc->execute_plan(); 623 $results = $bc->get_results(); 624 $file = $results['backup_destination']; 625 $bc->destroy(); 626 627 // Restore the backup immediately. 628 629 // Check if we need to unzip the file because the backup temp dir does not contains backup files. 630 if (!file_exists($backupbasepath . "/moodle_backup.xml")) { 631 $file->extract_to_pathname(get_file_packer('application/vnd.moodle.backup'), $backupbasepath); 632 } 633 634 if ($target == backup::TARGET_NEW_COURSE) { 635 $newcourseid = restore_dbops::create_new_course($course->fullname . '_2', 636 $course->shortname . '_2', 637 $course->category); 638 } else { 639 $newcourse = $this->getDataGenerator()->create_course(); 640 $newcourseid = $newcourse->id; 641 } 642 $rc = new restore_controller($backupid, $newcourseid, 643 backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $user->id, $target); 644 645 return [$course, $newcourseid, $rc]; 646 } 647 648 /** 649 * Backup a course with enrolment methods and restore it without user data and without enrolment methods 650 */ 651 public function test_restore_without_users_without_enrolments() { 652 global $DB; 653 654 list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE); 655 656 // Ensure enrolment methods will not be restored without capability. 657 $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value()); 658 $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value()); 659 660 $this->assertTrue($rc->execute_precheck()); 661 $rc->execute_plan(); 662 $rc->destroy(); 663 664 // Self-enrolment method was not enabled, users were not restored. 665 $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 666 'status' => ENROL_INSTANCE_ENABLED])); 667 $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue 668 join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; 669 $enrolments = $DB->get_records_sql($sql, [$newcourseid]); 670 $this->assertEmpty($enrolments); 671 } 672 673 /** 674 * Backup a course with enrolment methods and restore it without user data with enrolment methods 675 */ 676 public function test_restore_without_users_with_enrolments() { 677 global $DB; 678 679 list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE, 680 ['moodle/course:enrolconfig']); 681 682 // Ensure enrolment methods will be restored. 683 $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value()); 684 $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value()); 685 // Set "Include enrolment methods" to "Always" so they can be restored without users. 686 $rc->get_plan()->get_setting('enrolments')->set_value(backup::ENROL_ALWAYS); 687 688 $this->assertTrue($rc->execute_precheck()); 689 $rc->execute_plan(); 690 $rc->destroy(); 691 692 // Self-enrolment method was restored (it is enabled), users were not restored. 693 $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 694 'status' => ENROL_INSTANCE_ENABLED]); 695 $this->assertNotEmpty($enrol); 696 697 $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue 698 join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; 699 $enrolments = $DB->get_records_sql($sql, [$newcourseid]); 700 $this->assertEmpty($enrolments); 701 } 702 703 /** 704 * Backup a course with enrolment methods and restore it with user data and without enrolment methods 705 */ 706 public function test_restore_with_users_without_enrolments() { 707 global $DB; 708 709 list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE, 710 ['moodle/backup:userinfo', 'moodle/restore:userinfo']); 711 712 // Ensure enrolment methods will not be restored without capability. 713 $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value()); 714 $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); 715 716 global $qwerty; 717 $qwerty = 1; 718 $this->assertTrue($rc->execute_precheck()); 719 $rc->execute_plan(); 720 $rc->destroy(); 721 $qwerty = 0; 722 723 // Self-enrolment method was not restored, student was restored as manual enrolment. 724 $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 725 'status' => ENROL_INSTANCE_ENABLED])); 726 727 $enrol = $DB->get_record('enrol', ['enrol' => 'manual', 'courseid' => $newcourseid]); 728 $this->assertEquals(1, $DB->count_records('user_enrolments', ['enrolid' => $enrol->id])); 729 } 730 731 /** 732 * Backup a course with enrolment methods and restore it with user data with enrolment methods 733 */ 734 public function test_restore_with_users_with_enrolments() { 735 global $DB; 736 737 list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE, 738 ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']); 739 740 // Ensure enrolment methods will be restored. 741 $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value()); 742 $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); 743 744 $this->assertTrue($rc->execute_precheck()); 745 $rc->execute_plan(); 746 $rc->destroy(); 747 748 // Self-enrolment method was restored (it is enabled), student was restored. 749 $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 750 'status' => ENROL_INSTANCE_ENABLED]); 751 $this->assertNotEmpty($enrol); 752 753 $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue 754 join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; 755 $enrolments = $DB->get_records_sql($sql, [$newcourseid]); 756 $this->assertEquals(1, count($enrolments)); 757 $enrolment = reset($enrolments); 758 $this->assertEquals('self', $enrolment->enrol); 759 } 760 761 /** 762 * Backup a course with enrolment methods and restore it with user data with enrolment methods merging into another course 763 */ 764 public function test_restore_with_users_with_enrolments_merging() { 765 global $DB; 766 767 list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_ADDING, 768 ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']); 769 770 // Ensure enrolment methods will be restored. 771 $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value()); 772 $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); 773 774 $this->assertTrue($rc->execute_precheck()); 775 $rc->execute_plan(); 776 $rc->destroy(); 777 778 // User was restored with self-enrolment method. 779 $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 780 'status' => ENROL_INSTANCE_ENABLED]); 781 $this->assertNotEmpty($enrol); 782 783 $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue 784 join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; 785 $enrolments = $DB->get_records_sql($sql, [$newcourseid]); 786 $this->assertEquals(1, count($enrolments)); 787 $enrolment = reset($enrolments); 788 $this->assertEquals('self', $enrolment->enrol); 789 } 790 791 /** 792 * Backup a course with enrolment methods and restore it with user data with enrolment methods into another course deleting it's contents 793 */ 794 public function test_restore_with_users_with_enrolments_deleting() { 795 global $DB; 796 797 list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_DELETING, 798 ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']); 799 800 // Ensure enrolment methods will be restored. 801 $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value()); 802 $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); 803 804 $this->assertTrue($rc->execute_precheck()); 805 $rc->execute_plan(); 806 $rc->destroy(); 807 808 // Self-enrolment method was restored (it is enabled), student was restored. 809 $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 810 'status' => ENROL_INSTANCE_ENABLED]); 811 $this->assertNotEmpty($enrol); 812 813 $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue 814 join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; 815 $enrolments = $DB->get_records_sql($sql, [$newcourseid]); 816 $this->assertEquals(1, count($enrolments)); 817 $enrolment = reset($enrolments); 818 $this->assertEquals('self', $enrolment->enrol); 819 } 820 821 /** 822 * Test the block instance time fields (timecreated, timemodified) through a backup and restore. 823 */ 824 public function test_block_instance_times_backup() { 825 global $DB; 826 $this->resetAfterTest(); 827 828 $this->setAdminUser(); 829 $generator = $this->getDataGenerator(); 830 831 // Create course and add HTML block. 832 $course = $generator->create_course(); 833 $context = \context_course::instance($course->id); 834 $page = new \moodle_page(); 835 $page->set_context($context); 836 $page->set_course($course); 837 $page->set_pagelayout('standard'); 838 $page->set_pagetype('course-view'); 839 $page->blocks->load_blocks(); 840 $page->blocks->add_block_at_end_of_default_region('html'); 841 842 // Update (hack in database) timemodified and timecreated to specific values for testing. 843 $blockdata = $DB->get_record('block_instances', 844 ['blockname' => 'html', 'parentcontextid' => $context->id]); 845 $originalblockid = $blockdata->id; 846 $blockdata->timecreated = 12345; 847 $blockdata->timemodified = 67890; 848 $DB->update_record('block_instances', $blockdata); 849 850 // Do backup and restore. 851 $newcourseid = $this->backup_and_restore($course); 852 853 // Confirm that values were transferred correctly into HTML block on new course. 854 $newcontext = \context_course::instance($newcourseid); 855 $blockdata = $DB->get_record('block_instances', 856 ['blockname' => 'html', 'parentcontextid' => $newcontext->id]); 857 $this->assertEquals(12345, $blockdata->timecreated); 858 $this->assertEquals(67890, $blockdata->timemodified); 859 860 // Simulate what happens with an older backup that doesn't have those fields, by removing 861 // them from the backup before doing a restore. 862 $before = time(); 863 $newcourseid = $this->backup_and_restore($course, 0, function($backupid) use($originalblockid) { 864 global $CFG; 865 $path = $CFG->dataroot . '/temp/backup/' . $backupid . '/course/blocks/html_' . 866 $originalblockid . '/block.xml'; 867 $xml = file_get_contents($path); 868 $xml = preg_replace('~<timecreated>.*?</timemodified>~s', '', $xml); 869 file_put_contents($path, $xml); 870 }); 871 $after = time(); 872 873 // The fields not specified should default to current time. 874 $newcontext = \context_course::instance($newcourseid); 875 $blockdata = $DB->get_record('block_instances', 876 ['blockname' => 'html', 'parentcontextid' => $newcontext->id]); 877 $this->assertTrue($before <= $blockdata->timecreated && $after >= $blockdata->timecreated); 878 $this->assertTrue($before <= $blockdata->timemodified && $after >= $blockdata->timemodified); 879 } 880 881 /** 882 * When you restore a site with global search (or search indexing) turned on, then it should 883 * add entries to the search index requests table so that the data gets indexed. 884 */ 885 public function test_restore_search_index_requests() { 886 global $DB, $CFG, $USER; 887 888 $this->resetAfterTest(true); 889 $this->setAdminUser(); 890 $CFG->enableglobalsearch = true; 891 892 // Create a course. 893 $generator = $this->getDataGenerator(); 894 $course = $generator->create_course(); 895 896 // Add a forum. 897 $forum = $generator->create_module('forum', ['course' => $course->id]); 898 899 // Add a block. 900 $context = \context_course::instance($course->id); 901 $page = new \moodle_page(); 902 $page->set_context($context); 903 $page->set_course($course); 904 $page->set_pagelayout('standard'); 905 $page->set_pagetype('course-view'); 906 $page->blocks->load_blocks(); 907 $page->blocks->add_block_at_end_of_default_region('html'); 908 909 // Initially there should be no search index requests. 910 $this->assertEquals(0, $DB->count_records('search_index_requests')); 911 912 // Do backup and restore. 913 $newcourseid = $this->backup_and_restore($course); 914 915 // Now the course should be requested for index (all search areas). 916 $newcontext = \context_course::instance($newcourseid); 917 $requests = array_values($DB->get_records('search_index_requests')); 918 $this->assertCount(1, $requests); 919 $this->assertEquals($newcontext->id, $requests[0]->contextid); 920 $this->assertEquals('', $requests[0]->searcharea); 921 922 get_fast_modinfo($newcourseid); 923 924 // Backup the new course... 925 $CFG->backup_file_logger_level = backup::LOG_NONE; 926 $bc = new backup_controller(backup::TYPE_1COURSE, $newcourseid, 927 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, 928 $USER->id); 929 $backupid = $bc->get_backupid(); 930 $bc->execute_plan(); 931 $bc->destroy(); 932 933 // Restore it on top of old course (should duplicate the forum). 934 $rc = new restore_controller($backupid, $course->id, 935 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 936 backup::TARGET_EXISTING_ADDING); 937 $this->assertTrue($rc->execute_precheck()); 938 $rc->execute_plan(); 939 $rc->destroy(); 940 941 // Get the forums now on the old course. 942 $modinfo = get_fast_modinfo($course->id); 943 $forums = $modinfo->get_instances_of('forum'); 944 $this->assertCount(2, $forums); 945 946 // The newer one will be the one with larger ID. (Safe to assume for unit test.) 947 $biggest = null; 948 foreach ($forums as $forum) { 949 if ($biggest === null || $biggest->id < $forum->id) { 950 $biggest = $forum; 951 } 952 } 953 $restoredforumcontext = \context_module::instance($biggest->id); 954 955 // Get the HTML blocks now on the old course. 956 $blockdata = array_values($DB->get_records('block_instances', 957 ['blockname' => 'html', 'parentcontextid' => $context->id], 'id DESC')); 958 $restoredblockcontext = \context_block::instance($blockdata[0]->id); 959 960 // Check that we have requested index update on both the module and the block. 961 $requests = array_values($DB->get_records('search_index_requests', null, 'id')); 962 $this->assertCount(3, $requests); 963 $this->assertEquals($restoredblockcontext->id, $requests[1]->contextid); 964 $this->assertEquals('', $requests[1]->searcharea); 965 $this->assertEquals($restoredforumcontext->id, $requests[2]->contextid); 966 $this->assertEquals('', $requests[2]->searcharea); 967 } 968 969 /** 970 * Test restoring courses based on the backup plan. Primarily used with 971 * the import functionality 972 */ 973 public function test_restore_course_using_plan_defaults() { 974 global $DB, $CFG, $USER; 975 976 $this->resetAfterTest(true); 977 $this->setAdminUser(); 978 $CFG->enableglobalsearch = true; 979 980 // Set admin config setting so that activities are not restored by default. 981 set_config('restore_general_activities', 0, 'restore'); 982 983 // Create a course. 984 $generator = $this->getDataGenerator(); 985 $course = $generator->create_course(); 986 $course2 = $generator->create_course(); 987 $course3 = $generator->create_course(); 988 989 // Add a forum. 990 $forum = $generator->create_module('forum', ['course' => $course->id]); 991 992 // Backup course... 993 $CFG->backup_file_logger_level = backup::LOG_NONE; 994 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, 995 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, 996 $USER->id); 997 $backupid = $bc->get_backupid(); 998 $bc->execute_plan(); 999 $bc->destroy(); 1000 1001 // Restore it on top of course2 (should duplicate the forum). 1002 $rc = new restore_controller($backupid, $course2->id, 1003 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, 1004 backup::TARGET_EXISTING_ADDING, null, backup::RELEASESESSION_NO); 1005 $this->assertTrue($rc->execute_precheck()); 1006 $rc->execute_plan(); 1007 $rc->destroy(); 1008 1009 // Get the forums now on the old course. 1010 $modinfo = get_fast_modinfo($course2->id); 1011 $forums = $modinfo->get_instances_of('forum'); 1012 $this->assertCount(0, $forums); 1013 } 1014 1015 /** 1016 * The Question category hierarchical structure was changed in Moodle 3.5. 1017 * From 3.5, all question categories in each context are a child of a single top level question category for that context. 1018 * This test ensures that both Moodle 3.4 and 3.5 backups can still be correctly restored. 1019 */ 1020 public function test_restore_question_category_34_35() { 1021 global $DB, $USER, $CFG; 1022 1023 $this->resetAfterTest(true); 1024 $this->setAdminUser(); 1025 1026 $backupfiles = array('question_category_34_format', 'question_category_35_format'); 1027 1028 foreach ($backupfiles as $backupfile) { 1029 // Extract backup file. 1030 $backupid = $backupfile; 1031 $backuppath = make_backup_temp_directory($backupid); 1032 get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( 1033 __DIR__ . "/fixtures/$backupfile.mbz", $backuppath); 1034 1035 // Do restore to new course with default settings. 1036 $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); 1037 $newcourseid = restore_dbops::create_new_course( 1038 'Test fullname', 'Test shortname', $categoryid); 1039 $rc = new restore_controller($backupid, $newcourseid, 1040 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 1041 backup::TARGET_NEW_COURSE); 1042 1043 $this->assertTrue($rc->execute_precheck()); 1044 $rc->execute_plan(); 1045 $rc->destroy(); 1046 1047 // Get information about the resulting course and check that it is set up correctly. 1048 $modinfo = get_fast_modinfo($newcourseid); 1049 $quizzes = array_values($modinfo->get_instances_of('quiz')); 1050 $contexts = $quizzes[0]->context->get_parent_contexts(true); 1051 1052 $topcategorycount = []; 1053 foreach ($contexts as $context) { 1054 $cats = $DB->get_records('question_categories', array('contextid' => $context->id), 'parent', 'id, name, parent'); 1055 1056 // Make sure all question categories that were inside the backup file were restored correctly. 1057 if ($context->contextlevel == CONTEXT_COURSE) { 1058 $this->assertEquals(['top', 'Default for C101'], array_column($cats, 'name')); 1059 } else if ($context->contextlevel == CONTEXT_MODULE) { 1060 $this->assertEquals(['top', 'Default for Q1'], array_column($cats, 'name')); 1061 } 1062 1063 $topcategorycount[$context->id] = 0; 1064 foreach ($cats as $cat) { 1065 if (!$cat->parent) { 1066 $topcategorycount[$context->id]++; 1067 } 1068 } 1069 1070 // Make sure there is a single top level category in this context. 1071 if ($cats) { 1072 $this->assertEquals(1, $topcategorycount[$context->id]); 1073 } 1074 } 1075 } 1076 } 1077 1078 /** 1079 * Test the content bank content through a backup and restore. 1080 */ 1081 public function test_contentbank_content_backup() { 1082 global $DB, $USER, $CFG; 1083 $this->resetAfterTest(); 1084 1085 $this->setAdminUser(); 1086 $generator = $this->getDataGenerator(); 1087 $cbgenerator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); 1088 1089 // Create course and add content bank content. 1090 $course = $generator->create_course(); 1091 $context = \context_course::instance($course->id); 1092 $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; 1093 $contents = $cbgenerator->generate_contentbank_data('contenttype_h5p', 2, $USER->id, $context, true, $filepath); 1094 $this->assertEquals(2, $DB->count_records('contentbank_content')); 1095 1096 // Do backup and restore. 1097 $newcourseid = $this->backup_and_restore($course); 1098 1099 // Confirm that values were transferred correctly into content bank on new course. 1100 $newcontext = \context_course::instance($newcourseid); 1101 1102 $this->assertEquals(4, $DB->count_records('contentbank_content')); 1103 $this->assertEquals(2, $DB->count_records('contentbank_content', ['contextid' => $newcontext->id])); 1104 } 1105 1106 /** 1107 * Test the xAPI state through a backup and restore. 1108 * 1109 * @covers \backup_xapistate_structure_step 1110 * @covers \restore_xapistate_structure_step 1111 */ 1112 public function test_xapistate_backup() { 1113 global $DB; 1114 $this->resetAfterTest(); 1115 $this->setAdminUser(); 1116 1117 $course = $this->getDataGenerator()->create_course(); 1118 $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); 1119 $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); 1120 $this->setUser($user); 1121 1122 /** @var \mod_h5pactivity_generator $generator */ 1123 $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); 1124 1125 /** @var \core_h5p_generator $h5pgenerator */ 1126 $h5pgenerator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); 1127 1128 // Add an attempt to the H5P activity. 1129 $attemptinfo = [ 1130 'userid' => $user->id, 1131 'h5pactivityid' => $activity->id, 1132 'attempt' => 1, 1133 'interactiontype' => 'compound', 1134 'rawscore' => 2, 1135 'maxscore' => 2, 1136 'duration' => 1, 1137 'completion' => 1, 1138 'success' => 0, 1139 ]; 1140 $generator->create_attempt($attemptinfo); 1141 1142 // Add also a xAPI state to the H5P activity. 1143 $filerecord = [ 1144 'contextid' => \context_module::instance($activity->cmid)->id, 1145 'component' => 'mod_h5pactivity', 1146 'filearea' => 'package', 1147 'itemid' => 0, 1148 'filepath' => '/', 1149 'filepath' => '/', 1150 'filename' => 'dummy.h5p', 1151 'addxapistate' => true, 1152 ]; 1153 $h5pgenerator->generate_h5p_data(false, $filerecord); 1154 1155 // Check the H5P activity exists and the attempt has been created. 1156 $this->assertEquals(1, $DB->count_records('h5pactivity')); 1157 $this->assertEquals(2, $DB->count_records('grade_items')); 1158 $this->assertEquals(2, $DB->count_records('grade_grades')); 1159 $this->assertEquals(1, $DB->count_records('xapi_states')); 1160 1161 // Do backup and restore. 1162 $this->setAdminUser(); 1163 $newcourseid = $this->backup_and_restore($course, 0, null, true); 1164 1165 // Confirm that values were transferred correctly into H5P activity on new course. 1166 $this->assertEquals(2, $DB->count_records('h5pactivity')); 1167 $this->assertEquals(4, $DB->count_records('grade_items')); 1168 $this->assertEquals(4, $DB->count_records('grade_grades')); 1169 $this->assertEquals(2, $DB->count_records('xapi_states')); 1170 1171 $newactivity = $DB->get_record('h5pactivity', ['course' => $newcourseid]); 1172 $cm = get_coursemodule_from_instance('h5pactivity', $newactivity->id); 1173 $context = \context_module::instance($cm->id); 1174 $this->assertEquals(1, $DB->count_records('xapi_states', ['itemid' => $context->id])); 1175 } 1176 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body