diff --git a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php index 6bdb81376ff..3b5fc42ad03 100644 --- a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php +++ b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php @@ -40,6 +40,9 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st */ protected $sectioncreated = false; + /** @var stdClass|null $currentquizattempt Track the current quiz attempt being restored. */ + protected $currentquizattempt = null; + /** * @var bool when restoring old quizzes (2.8 or before) this records the * shufflequestionsoption quiz option which has moved to the quiz_sections table. @@ -453,7 +456,16 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st $data->quiz = $this->get_new_parentid('quiz'); $data->attempt = $data->attemptnum; - $data->userid = $this->get_mappingid('user', $data->userid); + // Get user mapping, return early if no mapping found for the quiz attempt. + $olduserid = $data->userid; + $data->userid = $this->get_mappingid('user', $olduserid, 0); + if ($data->userid === 0) { + $this->log('Mapped user ID not found for user ' . $olduserid . ', quiz ' . $this->get_new_parentid('quiz') . + ', attempt ' . $data->attempt . '. Skipping quiz attempt', backup::LOG_INFO); + + $this->currentquizattempt = null; + return; + } if (!empty($data->timecheckstate)) { $data->timecheckstate = $this->apply_date_offset($data->timecheckstate); @@ -488,6 +500,9 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st global $DB; $data = $this->currentquizattempt; + if ($data === null) { + return; + } $oldid = $data->id; $data->uniqueid = $newusageid; diff --git a/mod/quiz/tests/fixtures/question_attempts_missing_users.mbz b/mod/quiz/tests/fixtures/question_attempts_missing_users.mbz new file mode 100644 index 00000000000..2ddc2a66f59 Binary files /dev/null and b/mod/quiz/tests/fixtures/question_attempts_missing_users.mbz differ diff --git a/mod/quiz/tests/restore_attempt_test.php b/mod/quiz/tests/restore_attempt_test.php new file mode 100644 index 00000000000..71492d0cdb5 --- /dev/null +++ b/mod/quiz/tests/restore_attempt_test.php @@ -0,0 +1,101 @@ +. + +/** + * Unit tests restoring quiz attempts + * + * @package mod_quiz + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_quiz_restore_attempt_test extends advanced_testcase { + + /** + * Load required libraries + */ + public static function setUpBeforeClass(): void { + global $CFG; + + require_once("{$CFG->dirroot}/backup/util/includes/restore_includes.php"); + } + + /** + * Test restore dates. + */ + public function test_restore_question_attempts_missing_users(): void { + global $DB, $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + // Contains a quiz with four attempts from different users. The users are as follows (user ID -> user): + // 3 -> User 01, 4 -> User 02, 5 -> User 03, 6 -> User 04. + // The user details for User 02 and User 03 have been removed from the backup file. + $testfixture = __DIR__ . '/fixtures/question_attempts_missing_users.mbz'; + + // Extract our test fixture, ready to be restored. + $backuptempdir = 'aaa'; + $backuppath = make_backup_temp_directory($backuptempdir); + get_file_packer('application/vnd.moodle.backup')->extract_to_pathname($testfixture, $backuppath); + + // Do the restore to new course with default settings. + $categoryid = $DB->get_field('course_categories', 'MIN(id)', []); + $courseid = restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid); + + $controller = new restore_controller($backuptempdir, $courseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, + backup::TARGET_NEW_COURSE); + + $this->assertTrue($controller->execute_precheck()); + $controller->execute_plan(); + $controller->destroy(); + + // Grade restore also generates some debugging. + $this->assertDebuggingCalledCount(2); + + $restoredquiz = $DB->get_record('quiz', []); + + // Assert logs were added for User 02 and User 03, due to missing mapping lookup. + $loginfomessages = $DB->get_fieldset_select('backup_logs', 'message', 'backupid = ? AND loglevel = ?', [ + $controller->get_restoreid(), + backup::LOG_INFO, + ]); + + $this->assertContains("Mapped user ID not found for user 4, quiz {$restoredquiz->id}, attempt 1. Skipping quiz attempt", + $loginfomessages); + $this->assertContains("Mapped user ID not found for user 5, quiz {$restoredquiz->id}, attempt 1. Skipping quiz attempt", + $loginfomessages); + + // User 01 has supplied the wrong answer, assert dates match the backup file too. + $user01attempt = $DB->get_record('quiz_attempts', [ + 'quiz' => $restoredquiz->id, + 'userid' => core_user::get_user_by_username('user01')->id, + ]); + + $this->assertEquals(1634751274, $user01attempt->timestart); + $this->assertEquals(1634751290, $user01attempt->timefinish); + $this->assertEquals(0.0, (float) $user01attempt->sumgrades); + + // User 04 has supplied the correct answer, assert dates match the backup file too. + $user04attempt = $DB->get_record('quiz_attempts', [ + 'quiz' => $restoredquiz->id, + 'userid' => core_user::get_user_by_username('user04')->id, + ]); + + $this->assertEquals(1634751341, $user04attempt->timestart); + $this->assertEquals(1634751347, $user04attempt->timefinish); + $this->assertEquals(1.0, (float) $user04attempt->sumgrades); + } +}