From f910e87c120f7b5c633aeba0481fa0e7111fa635 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 15 Jul 2019 13:15:03 +0800 Subject: [PATCH 1/2] MDL-66021 backup: Clear course if required before restore --- .../controller/restore_controller.class.php | 23 +++++++++++++++++++ backup/util/ui/restore_ui.class.php | 7 +----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/backup/controller/restore_controller.class.php b/backup/controller/restore_controller.class.php index b709f7840ec..1f63e577cc3 100644 --- a/backup/controller/restore_controller.class.php +++ b/backup/controller/restore_controller.class.php @@ -332,6 +332,21 @@ class restore_controller extends base_controller { public function get_plan() { return $this->plan; } + /** + * Gets the value for the requested setting + * + * @param string $name + * @param bool $default + * @return mixed + */ + public function get_setting_value($name, $default = false) { + try { + return $this->get_plan()->get_setting($name)->get_value(); + } catch (Exception $e) { + debugging('Failed to find the setting: '.$name, DEBUG_DEVELOPER); + return $default; + } + } public function get_info() { return $this->info; @@ -341,6 +356,14 @@ class restore_controller extends base_controller { // Basic/initial prevention against time/memory limits core_php_time_limit::raise(1 * 60 * 60); // 1 hour for 1 course initially granted raise_memory_limit(MEMORY_EXTRA); + + // Do course cleanup precheck, if required. This was originally in restore_ui. Moved to handle async backup/restore. + if ($this->get_target() == backup::TARGET_CURRENT_DELETING || $this->get_target() == backup::TARGET_EXISTING_DELETING) { + $options = array(); + $options['keep_roles_and_enrolments'] = $this->get_setting_value('keep_roles_and_enrolments'); + $options['keep_groups_and_groupings'] = $this->get_setting_value('keep_groups_and_groupings'); + restore_dbops::delete_course_content($this->get_courseid(), $options); + } // If this is not a course restore or single activity restore (e.g. duplicate), inform the plan we are not // including all the activities for sure. This will affect any // task/step executed conditionally to stop processing information diff --git a/backup/util/ui/restore_ui.class.php b/backup/util/ui/restore_ui.class.php index 7c0de1bc27b..98a105704a3 100644 --- a/backup/util/ui/restore_ui.class.php +++ b/backup/util/ui/restore_ui.class.php @@ -215,12 +215,7 @@ class restore_ui extends base_ui { if ($this->stage->get_stage() < self::STAGE_PROCESS) { throw new restore_ui_exception('restoreuifinalisedbeforeexecute'); } - if ($this->controller->get_target() == backup::TARGET_CURRENT_DELETING || $this->controller->get_target() == backup::TARGET_EXISTING_DELETING) { - $options = array(); - $options['keep_roles_and_enrolments'] = $this->get_setting_value('keep_roles_and_enrolments'); - $options['keep_groups_and_groupings'] = $this->get_setting_value('keep_groups_and_groupings'); - restore_dbops::delete_course_content($this->controller->get_courseid(), $options); - } + $this->controller->execute_plan(); $this->progress = self::PROGRESS_EXECUTED; $this->stage = new restore_ui_stage_complete($this, $this->stage->get_params(), $this->controller->get_results()); From 62566af752de7cf087d68d90ade2a524c71ca9ef Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 17 Jul 2019 10:43:58 +0800 Subject: [PATCH 2/2] MDL-66021 restore: Additional restore unit tests with async mode --- course/tests/restore_test.php | 151 ++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/course/tests/restore_test.php b/course/tests/restore_test.php index 470e03b13da..4f659902ffb 100644 --- a/course/tests/restore_test.php +++ b/course/tests/restore_test.php @@ -131,6 +131,157 @@ class core_course_restore_testcase extends advanced_testcase { return $this->restore_course($backupid, 0, $userid); } + /** + * Restore a course. + * + * @param int $backupid The backup ID. + * @param int $courseid The course ID to restore in, or 0. + * @param int $userid The ID of the user performing the restore. + * @param int $target THe target of the restore. + * + * @return stdClass The updated course object. + */ + protected function async_restore_course($backupid, $courseid, $userid, $target) { + global $DB; + + if (!$courseid) { + $target = backup::TARGET_NEW_COURSE; + $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); + $courseid = restore_dbops::create_new_course('Tmp', 'tmp', $categoryid); + } + + $rc = new restore_controller($backupid, $courseid, backup::INTERACTIVE_NO, backup::MODE_ASYNC, $userid, $target); + $target == backup::TARGET_NEW_COURSE ?: $rc->get_plan()->get_setting('overwrite_conf')->set_value(true); + $this->assertTrue($rc->execute_precheck()); + + $restoreid = $rc->get_restoreid(); + $rc->destroy(); + + // Create the adhoc task. + $asynctask = new \core\task\asynchronous_restore_task(); + $asynctask->set_blocking(false); + $asynctask->set_custom_data(array('backupid' => $restoreid)); + \core\task\manager::queue_adhoc_task($asynctask); + + // We are expecting trace output during this test. + $this->expectOutputRegex("/$restoreid/"); + + // Execute adhoc task. + $now = time(); + $task = \core\task\manager::get_next_adhoc_task($now); + $this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task); + $task->execute(); + \core\task\manager::adhoc_task_complete($task); + + $course = $DB->get_record('course', array('id' => $rc->get_courseid())); + + return $course; + } + + /** + * Restore a course to an existing course. + * + * @param int $backupid The backup ID. + * @param int $courseid The course ID to restore in. + * @param int $userid The ID of the user performing the restore. + * @param int $target The type of restore we are performing. + * @return stdClass The updated course object. + */ + protected function async_restore_to_existing_course($backupid, $courseid, + $userid = 2, $target = backup::TARGET_CURRENT_ADDING) { + return $this->async_restore_course($backupid, $courseid, $userid, $target); + } + + /** + * Restore a course to a new course. + * + * @param int $backupid The backup ID. + * @param int $userid The ID of the user performing the restore. + * @return stdClass The new course object. + */ + protected function async_restore_to_new_course($backupid, $userid = 2) { + return $this->async_restore_course($backupid, 0, $userid, 0); + } + + public function test_async_restore_existing_idnumber_in_new_course() { + $this->resetAfterTest(); + + $dg = $this->getDataGenerator(); + $c1 = $dg->create_course(['idnumber' => 'ABC']); + $backupid = $this->backup_course($c1->id); + $c2 = $this->async_restore_to_new_course($backupid); + + // The ID number is set empty. + $this->assertEquals('', $c2->idnumber); + } + + public function test_async_restore_course_info_in_existing_course() { + global $DB; + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + + $this->assertEquals(1, get_config('restore', 'restore_merge_course_shortname')); + $this->assertEquals(1, get_config('restore', 'restore_merge_course_fullname')); + $this->assertEquals(1, get_config('restore', 'restore_merge_course_startdate')); + + $startdate = mktime(12, 0, 0, 7, 1, 2016); // 01-Jul-2016. + + // Create two courses with different start dates,in each course create a chat that opens 1 week after the course start date. + $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE, + 'startdate' => $startdate]); + $chat1 = $dg->create_module('chat', ['name' => 'First', 'course' => $c1->id, 'chattime' => $c1->startdate + 1 * WEEKSECS]); + $c2 = $dg->create_course(['shortname' => 'A', 'fullname' => 'B', 'summary' => 'C', 'summaryformat' => FORMAT_PLAIN, + 'startdate' => $startdate + 2 * WEEKSECS]); + $chat2 = $dg->create_module('chat', ['name' => 'Second', 'course' => $c2->id, 'chattime' => $c2->startdate + 1 * WEEKSECS]); + $backupid = $this->backup_course($c1->id); + + // The information is restored but adapted because names are already taken. + $c2 = $this->async_restore_to_existing_course($backupid, $c2->id); + $this->assertEquals('SN_1', $c2->shortname); + $this->assertEquals('FN copy 1', $c2->fullname); + $this->assertEquals('DESC', $c2->summary); + $this->assertEquals(FORMAT_MOODLE, $c2->summaryformat); + $this->assertEquals($startdate, $c2->startdate); + + // Now course c2 has two chats - one ('Second') was already there and one ('First') was restored from the backup. + // Their dates are exactly the same as they were in the original modules. + $restoredchat1 = $DB->get_record('chat', ['name' => 'First', 'course' => $c2->id]); + $restoredchat2 = $DB->get_record('chat', ['name' => 'Second', 'course' => $c2->id]); + $this->assertEquals($chat1->chattime, $restoredchat1->chattime); + $this->assertEquals($chat2->chattime, $restoredchat2->chattime); + } + + public function test_async_restore_course_info_in_existing_course_delete_first() { + global $DB; + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + + $this->assertEquals(1, get_config('restore', 'restore_merge_course_shortname')); + $this->assertEquals(1, get_config('restore', 'restore_merge_course_fullname')); + $this->assertEquals(1, get_config('restore', 'restore_merge_course_startdate')); + + $startdate = mktime(12, 0, 0, 7, 1, 2016); // 01-Jul-2016. + + // Create two courses with different start dates,in each course create a chat that opens 1 week after the course start date. + $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE, + 'startdate' => $startdate]); + $chat1 = $dg->create_module('chat', ['name' => 'First', 'course' => $c1->id, 'chattime' => $c1->startdate + 1 * WEEKSECS]); + $c2 = $dg->create_course(['shortname' => 'A', 'fullname' => 'B', 'summary' => 'C', 'summaryformat' => FORMAT_PLAIN, + 'startdate' => $startdate + 2 * WEEKSECS]); + $chat2 = $dg->create_module('chat', ['name' => 'Second', 'course' => $c2->id, 'chattime' => $c2->startdate + 1 * WEEKSECS]); + $backupid = $this->backup_course($c1->id); + + // The information is restored and the existing course settings is modified. + $c2 = $this->async_restore_to_existing_course($backupid, $c2->id, 2, backup::TARGET_CURRENT_DELETING); + $this->assertEquals(FORMAT_MOODLE, $c2->summaryformat); + + // Now course2 should have a new forum with the original forum deleted. + $restoredchat1 = $DB->get_record('chat', ['name' => 'First', 'course' => $c2->id]); + $restoredchat2 = $DB->get_record('chat', ['name' => 'Second', 'course' => $c2->id]); + $this->assertEquals($chat1->chattime, $restoredchat1->chattime); + $this->assertEmpty($restoredchat2); + } + public function test_restore_existing_idnumber_in_new_course() { $this->resetAfterTest();