diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 3f72c09ef8e..12506966ee7 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -5482,8 +5482,25 @@ class restore_move_module_questions_categories extends restore_execution_step { foreach ($contexts as $contextid => $contextlevel) { if (!$newcontext = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $contextid)) { // The bank for the question categories required by this module was not included in the backup, - // but if that context still exists on the site then don't move them. - if (context::instance_by_id($contextid, IGNORE_MISSING)) { + // but if that context still exists on the site and the user has access then point question references + // to the originals. + $originalcontext = context::instance_by_id($contextid, IGNORE_MISSING); + if ($originalcontext && has_capability('mod/qbank:view', $originalcontext)) { + $originalquestions = get_questions_category(question_get_top_category($contextid), false); + foreach ($originalquestions as $originalquestion) { + $backupids = restore_dbops::get_backup_ids_record( + $this->get_restoreid(), + 'question', + $originalquestion->id, + ); + $DB->set_field_select( + 'question_references', + 'questionbankentryid', + $DB->get_field('question_versions', 'questionbankentryid', ['questionid' => $backupids->itemid]), + 'questionbankentryid = (SELECT questionbankentryid FROM {question_versions} WHERE questionid = ?)', + [$backupids->newitemid], + ); + } continue; } // We have no target question bank so create a default bank for categories without a module to attach to. @@ -5588,6 +5605,20 @@ class restore_move_module_questions_categories extends restore_execution_step { ); } } + // Remove any remaining course-level question categories from the restored course. + $coursecatsql = " + SELECT qc.id AS categoryid + FROM {question_categories} qc + JOIN {context} c ON c.id = qc.contextid + WHERE c.contextlevel = :courselevel AND c.instanceid = :courseid + "; + $DB->delete_records_subquery( + 'question_categories', + 'id', + 'categoryid', + $coursecatsql, + ['courselevel' => context_course::LEVEL, 'courseid' => $this->task->get_courseid()] + ); } } diff --git a/question/tests/backup_test.php b/question/tests/backup_test.php index af5bcc4929f..49e4c48fa69 100644 --- a/question/tests/backup_test.php +++ b/question/tests/backup_test.php @@ -17,6 +17,7 @@ namespace core_question; use context_course; +use mod_quiz\quiz_settings; use moodle_url; use question_bank; @@ -40,6 +41,7 @@ require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \restore_qtype_plugin * @covers \restore_create_categories_and_questions + * @covers \restore_move_module_questions_categories */ final class backup_test extends \advanced_testcase { @@ -545,11 +547,9 @@ final class backup_test extends \advanced_testcase { /** * If the backup contains ONLY a quiz but that quiz uses questions from a qbank module and itself, + * and the original course does not exist on the target system, * then the non-quiz context categories and questions should restore to a default qbank module on the new course * if the old qbank no longer exists. - * - * @return void - * @covers \restore_controller::execute_plan() */ public function test_quiz_activity_restore_to_new_course(): void { global $DB; @@ -605,6 +605,117 @@ final class backup_test extends \advanced_testcase { $this->assertEquals('bank question', $bankq->name); } + /** + * If the backup contains ONLY a quiz but that quiz uses questions from a qbank module and itself, + * and the original course does exist on the target system but you dont have permission to view the original qbank, + * then the non-quiz context categories and questions should restore to a default qbank module on the new course + * if the old qbank no longer exists. + */ + public function test_quiz_activity_restore_to_new_course_no_permission(): void { + global $DB; + + $this->resetAfterTest(); + self::setAdminUser(); + + // Create a course to make a backup. + $data = $this->add_course_quiz_and_qbank(); + $oldquiz = $data->quiz; + + // Backup ONLY the quiz module. + $backupid = $this->backup_course_module($oldquiz->cmid); + + // Create a new course to restore to. + $newcourse = self::getDataGenerator()->create_course(); + $restoreuser = self::getDataGenerator()->create_user(); + self::getDataGenerator()->enrol_user($restoreuser->id, $newcourse->id, 'manager'); + $this->setUser($restoreuser); + + $this->restore_to_course($backupid, $newcourse->id); + $modinfo = get_fast_modinfo($newcourse); + + // Assert we have our quiz including the category and question. + $newquizzes = $modinfo->get_instances_of('quiz'); + $this->assertCount(1, $newquizzes); + $newquiz = reset($newquizzes); + $newquizcontext = \context_module::instance($newquiz->id); + + $quizcats = $DB->get_records_select('question_categories', + 'parent <> 0 AND contextid = :contextid', + ['contextid' => $newquizcontext->id] + ); + $this->assertCount(1, $quizcats); + $quizcat = reset($quizcats); + $quizcatqs = get_questions_category($quizcat, false); + $this->assertCount(1, $quizcatqs); + $quizq = reset($quizcatqs); + $this->assertEquals('quiz question', $quizq->name); + + // The backup did not contain the qbank that held the categories, but it is dependant. + // So make sure the categories and questions got restored to a qbank module on the course. + $defaultbanks = $modinfo->get_instances_of('qbank'); + $this->assertCount(1, $defaultbanks); + $defaultbank = reset($defaultbanks); + $defaultbankcontext = \context_module::instance($defaultbank->id); + $bankcats = $DB->get_records_select('question_categories', + 'parent <> 0 AND contextid = :contextid', + ['contextid' => $defaultbankcontext->id] + ); + $bankcat = reset($bankcats); + $bankqs = get_questions_category($bankcat, false); + $this->assertCount(1, $bankqs); + $bankq = reset($bankqs); + $this->assertEquals('bank question', $bankq->name); + } + + /** + * If the backup contains ONLY a quiz but that quiz uses questions from a qbank module and itself, + * and that qbank still exists on the system, and the restoring user can access that qbank, then + * the quiz should be restored with a copy of the quiz question, and a reference to the original qbank question. + */ + public function test_quiz_activity_restore_to_new_course_by_reference(): void { + global $DB; + + $this->resetAfterTest(); + self::setAdminUser(); + + // Create a course to make a backup. + $data = $this->add_course_quiz_and_qbank(); + $oldquiz = $data->quiz; + + // Backup ONLY the quiz module. + $backupid = $this->backup_course_module($oldquiz->cmid); + + // Create a new course to restore to. + $newcourse = self::getDataGenerator()->create_course(); + + $this->restore_to_course($backupid, $newcourse->id); + $modinfo = get_fast_modinfo($newcourse); + + // Assert we have our new quiz with the expected questions. + $newquizzes = $modinfo->get_instances_of('quiz'); + $this->assertCount(1, $newquizzes); + /** @var \cm_info $newquiz */ + $newquiz = reset($newquizzes); + $quiz = $DB->get_record('quiz', ['id' => $newquiz->instance], '*', MUST_EXIST); + [$course, $cm] = get_course_and_cm_from_instance($quiz, 'quiz'); + $newquizsettings = new quiz_settings($quiz, $cm, $course); + $newq1 = $newquizsettings->get_structure()->get_question_in_slot(1); + $newq2 = $newquizsettings->get_structure()->get_question_in_slot(2); + + $newquizcontext = \context_module::instance($newquiz->id); + $qbankcontext = \context_module::instance($data->qbank->cmid); + + // Check we've got a copy of the quiz question in the new context. + $this->assertEquals($data->quizquestion->name, $newq2->name); + $this->assertEquals($newquizcontext->id, $newq2->contextid); + // Check we've got a reference to the qbank question in the original context. + $this->assertEquals($data->qbankquestion->name, $newq1->name); + $this->assertEquals($qbankcontext->id, $newq1->contextid); + // Check we have the expected restored categories. + $this->assertEquals(2, $DB->count_records('question_categories', ['stamp' => $data->quizcategory->stamp])); + $this->assertEquals(1, $DB->count_records('question_categories', ['stamp' => $data->qbankcategory->stamp])); + } + /** * If the backup contains BOTH a quiz and a qbank module and the quiz uses questions from the qbank module and itself, * then we need to restore the categories and questions to the qbank and quiz modules included in the backup on the new course.