From 7f5b6adac18c97ac8d4cc8704ea61d4f08d24ad1 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Fri, 13 Mar 2020 17:22:27 +0100 Subject: [PATCH] MDL-67713 mod_h5pactivity: adding backup and restore --- .../moodle2/backup_h5pactivity_stepslib.php | 28 +++ .../moodle2/restore_h5pactivity_stepslib.php | 41 +++- mod/h5pactivity/tests/restore_test.php | 212 ++++++++++++++++++ 3 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 mod/h5pactivity/tests/restore_test.php diff --git a/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php b/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php index 8f40e330387..636e3aeac1f 100644 --- a/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php +++ b/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php @@ -44,10 +44,38 @@ class backup_h5pactivity_activity_structure_step extends backup_activity_structu 'introformat', 'grade', 'displayoptions']; $root = new backup_nested_element('h5pactivity', $attributes, $finalelements); + $attempts = new backup_nested_element('attempts'); + + $attempt = new backup_nested_element('attempt', ['id'], + ['h5pactivityid', 'userid', 'timecreated', 'timemodified', 'attempt', 'rawscore', 'maxscore'] + ); + + $results = new backup_nested_element('results'); + + $result = new backup_nested_element('result', ['id'], + [ + 'attemptid', 'subcontent', 'timecreated', 'interactiontype', 'description', + 'correctpattern', 'response', 'additionals', 'rawscore', 'maxscore' + ] + ); + + // Build the tree. + $root->add_child($attempts); + $attempts->add_child($attempt); + $attempt->add_child($results); + $results->add_child($result); + // Define the source tables for the elements. $root->set_source_table('h5pactivity', ['id' => backup::VAR_ACTIVITYID]); + // All the rest of elements only happen if we are including user info. + if ($userinfo) { + $attempt->set_source_table('h5pactivity_attempts', ['h5pactivityid' => backup::VAR_PARENTID], 'id ASC'); + $result->set_source_table('h5pactivity_attempts_results', ['attemptid' => backup::VAR_PARENTID], 'id ASC'); + } + // Define id annotations. + $attempt->annotate_ids('user', 'userid'); // Define file annotations. $root->annotate_files('mod_h5pactivity', 'intro', null); // This file area hasn't itemid. diff --git a/mod/h5pactivity/backup/moodle2/restore_h5pactivity_stepslib.php b/mod/h5pactivity/backup/moodle2/restore_h5pactivity_stepslib.php index 4c4ec6a3dad..3bd63d39627 100644 --- a/mod/h5pactivity/backup/moodle2/restore_h5pactivity_stepslib.php +++ b/mod/h5pactivity/backup/moodle2/restore_h5pactivity_stepslib.php @@ -38,17 +38,21 @@ class restore_h5pactivity_activity_structure_step extends restore_activity_struc $paths = []; $userinfo = $this->get_setting_value('userinfo'); $paths[] = new restore_path_element('h5pactivity', '/activity/h5pactivity'); + if ($userinfo) { + $paths[] = new restore_path_element('h5pactivity_attempt', '/activity/h5pactivity/attempts/attempt'); + $paths[] = new restore_path_element('h5pactivity_attempt_result', '/activity/h5pactivity/attempts/attempt/results/result'); + } return $this->prepare_activity_structure($paths); } /** - * Processes the elt restore data. + * Processes the h5pactivity restore data. * * @param array $data Parsed element data. */ protected function process_h5pactivity(array $data): void { global $DB; - $data = (object) $data; + $data = (object)$data; $data->course = $this->get_courseid(); // Insert the record. $newitemid = $DB->insert_record('h5pactivity', $data); @@ -56,6 +60,39 @@ class restore_h5pactivity_activity_structure_step extends restore_activity_struc $this->apply_activity_instance($newitemid); } + /** + * Processes the h5pactivity_attempts restore data. + * + * @param array $data Parsed element data. + */ + protected function process_h5pactivity_attempt(array $data): void { + global $DB; + $data = (object)$data; + + $oldid = $data->id; + $data->h5pactivityid = $this->get_new_parentid('h5pactivity'); + $data->userid = $this->get_mappingid('user', $data->userid); + + $newitemid = $DB->insert_record('h5pactivity_attempts', $data); + $this->set_mapping('h5pactivity_attempt', $oldid, $newitemid); + } + + /** + * Processes the h5pactivity_attempts_results restore data. + * + * @param array $data Parsed element data. + */ + protected function process_h5pactivity_attempt_result(array $data): void { + global $DB; + $data = (object)$data; + + $oldid = $data->id; + $data->attemptid = $this->get_new_parentid('h5pactivity_attempt'); + + $newitemid = $DB->insert_record('h5pactivity_attempts_results', $data); + $this->set_mapping('h5pactivity_attempt_result', $oldid, $newitemid); + } + /** * Defines post-execution actions. */ diff --git a/mod/h5pactivity/tests/restore_test.php b/mod/h5pactivity/tests/restore_test.php new file mode 100644 index 00000000000..95664b54da7 --- /dev/null +++ b/mod/h5pactivity/tests/restore_test.php @@ -0,0 +1,212 @@ +. + +/** + * mod_h5pactivity generator tests + * + * @package mod_h5pactivity + * @category test + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_h5pactivity; + +use advanced_testcase; +use backup; +use backup_controller; +use backup_setting; +use restore_controller; +use restore_dbops; +use stdClass; + +/** + * Genarator tests class for mod_h5pactivity. + * + * @package mod_h5pactivity + * @category test + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_h5pactivity_restore_testcase extends advanced_testcase { + + /** + * Setup to ensure that fixtures are loaded. + */ + public static function setupBeforeClass(): void { + global $CFG; + require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); + require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); + } + + /** + * Test on H5P activity backup and restore. + * + * @dataProvider backup_restore_data + * @param bool $content if has to create attempts + * @param bool $userdata if backup have userdata + * @param array $result1 data to check on original course + * @param array $result2 data to check on resotred course + */ + public function test_backup_restore(bool $content, bool $userdata, array $result1, array $result2) { + global $DB; + $this->resetAfterTest(); + + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + // Create one activity. + $this->assertFalse($DB->record_exists('h5pactivity', ['course' => $course->id])); + $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); + $cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST); + + if ($content) { + $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); + $params = ['cmid' => $cm->id, 'userid' => $user->id]; + $generator->create_content($activity, $params); + } + + $this->assertEquals($result1[0], $DB->count_records('h5pactivity', ['course' => $course->id])); + $this->assertEquals($result1[1], $DB->count_records('h5pactivity_attempts', ['h5pactivityid' => $activity->id])); + $attemptid = $DB->get_field('h5pactivity_attempts', 'id', ['h5pactivityid' => $activity->id]); + $this->assertEquals($result1[2], $DB->count_records('h5pactivity_attempts_results', ['attemptid' => $attemptid])); + + // Execute course backup and restore. + $newcourseid = $this->backup_and_restore($course, $userdata); + + // Check original activity. + $this->assertEquals($result1[0], $DB->count_records('h5pactivity', ['course' => $course->id])); + $this->assertEquals($result1[1], $DB->count_records('h5pactivity_attempts', ['h5pactivityid' => $activity->id])); + $attempt = $DB->get_record('h5pactivity_attempts', ['h5pactivityid' => $activity->id]); + $attemptid = $attempt->id ?? 0; + $this->assertEquals($result1[2], $DB->count_records('h5pactivity_attempts_results', ['attemptid' => $attemptid])); + + // Check original activity. + $this->assertEquals($result2[0], $DB->count_records('h5pactivity', ['course' => $newcourseid])); + $activity2 = $DB->get_record('h5pactivity', ['course' => $newcourseid]); + $this->assertEquals($result2[1], $DB->count_records('h5pactivity_attempts', ['h5pactivityid' => $activity2->id])); + $attempt2 = $DB->get_record('h5pactivity_attempts', ['h5pactivityid' => $activity2->id]); + $attempt2id = $attempt2->id ?? 0; + $this->assertEquals($result2[2], $DB->count_records('h5pactivity_attempts_results', ['attemptid' => $attempt2id])); + + // Compare activities. + $this->assertEquals($newcourseid, $activity2->course); + $this->assertEquals($activity->name, $activity2->name); + $this->assertEquals($activity->intro, $activity2->intro); + $this->assertEquals($activity->introformat, $activity2->introformat); + $this->assertEquals($activity->grade, $activity2->grade); + $this->assertEquals($activity->displayoptions, $activity2->displayoptions); + + // Compare attempts. + if ($content && $userdata) { + $this->assertEquals($activity2->id, $attempt2->h5pactivityid); + $this->assertEquals($attempt->userid, $attempt2->userid); + $this->assertEquals($attempt->timecreated, $attempt2->timecreated); + $this->assertEquals($attempt->timemodified, $attempt2->timemodified); + $this->assertEquals($attempt->attempt, $attempt2->attempt); + $this->assertEquals($attempt->rawscore, $attempt2->rawscore); + $this->assertEquals($attempt->maxscore, $attempt2->maxscore); + + // Compare results. + $results = $DB->get_records('h5pactivity_attempts_results', ['attemptid' => $attempt->id]); + foreach ($results as $result) { + $result2 = $DB->get_record('h5pactivity_attempts_results', [ + 'subcontent' => $result->subcontent, 'attemptid' => $attempt2->id + ]); + $this->assertNotFalse($result2); + $this->assertEquals($result->timecreated, $result2->timecreated); + $this->assertEquals($result->interactiontype, $result2->interactiontype); + $this->assertEquals($result->description, $result2->description); + $this->assertEquals($result->correctpattern, $result2->correctpattern); + $this->assertEquals($result->response, $result2->response); + $this->assertEquals($result->additionals, $result2->additionals); + $this->assertEquals($result->rawscore, $result2->rawscore); + $this->assertEquals($result->maxscore, $result2->maxscore); + } + } + + } + + /** + * Data provider for test_backup_restore. + * + * @return array + */ + public function backup_restore_data(): array { + return [ + 'Activity attempts and restore with userdata' => [ + true, true, [1, 1, 3], [1, 1, 3] + ], + 'No activity attempts and restore with userdata' => [ + false, true, [1, 0, 0], [1, 0, 0] + ], + 'Activity attempts and restore with no userdata' => [ + true, false, [1, 1, 3], [1, 0, 0] + ], + 'No activity attempts and restore with no userdata' => [ + false, false, [1, 0, 0], [1, 0, 0] + ], + ]; + } + + /** + * Backs a course up and restores it. + * + * @param stdClass $srccourse Course object to backup + * @param bool $userdata if the backup must be with user data + * @return int ID of newly restored course + */ + private function backup_and_restore(stdClass $srccourse, bool $userdata): int { + global $USER, $CFG; + + require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); + + // Turn off file logging, otherwise it can't delete the file (Windows). + $CFG->backup_file_logger_level = backup::LOG_NONE; + + // Do backup with default settings. MODE_IMPORT means it will just + // create the directory and not zip it. + $bc = new backup_controller(backup::TYPE_1COURSE, $srccourse->id, + backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, + $USER->id); + + $bc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED); + $bc->get_plan()->get_setting('users')->set_value($userdata); + + $backupid = $bc->get_backupid(); + $bc->execute_plan(); + $bc->destroy(); + + // Do restore to new course with default settings. + $newcourseid = restore_dbops::create_new_course( + $srccourse->fullname, $srccourse->shortname . '_2', $srccourse->category + ); + $rc = new restore_controller($backupid, $newcourseid, + backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, + backup::TARGET_NEW_COURSE); + + $rc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED); + $rc->get_plan()->get_setting('users')->set_value($userdata); + + $this->assertTrue($rc->execute_precheck()); + $rc->execute_plan(); + $rc->destroy(); + + return $newcourseid; + } +}