MDL-68806 quiz: Update questions in pre-created attempts when started

In case questions used in pre-created attempts were edited after
pre-creation, we check when the attempt is started and create
new question attempts if needed. There is still an opportunity to
improve performance further by finding a way to do this efficiently
when a new question version is created.
This commit is contained in:
Mark Johnson
2025-03-28 09:34:59 +00:00
parent 2e2e3adbbd
commit 50486f5d05
3 changed files with 98 additions and 0 deletions
+2
View File
@@ -372,6 +372,8 @@ function quiz_attempt_save_started(
$originalattempt = null;
if (isset($attempt->id) && $attempt->state === quiz_attempt::NOT_STARTED) {
$originalattempt = clone $attempt;
// In case questions have been edited since attempts were pre-created, update questions now.
quiz_attempt::create($attempt->id)->update_questions_to_new_version_if_changed();
// Update the attempt's state.
$attempt->state = quiz_attempt::IN_PROGRESS;
$DB->update_record('quiz_attempts', $attempt);
+17
View File
@@ -571,4 +571,21 @@ final class attempt_test extends \advanced_testcase {
$this->assertEquals(0, $grades[$readinggrade->id]->grade);
$this->assertEquals(1, $grades[$readinggrade->id]->maxgrade);
}
/**
* When creating a new quiz attempt, question attempts should be created with the first step's timecreated set to null.
*
* When the question attempt is rendered, it should be set to the current time.
*
* @return void
* @throws \coding_exception
* @covers ::quiz_start_new_attempt
*/
public function test_step_timecreated_unset_when_starting_quiz_attempt(): void {
$attempt = $this->create_quiz_and_attempt_with_layout('1');
$questionattempt = $attempt->get_question_attempt(1);
$this->assertEquals(\question_attempt_step::TIMECREATED_ON_FIRST_RENDER, $questionattempt->get_step(0)->get_timecreated());
$questionattempt->render(new \question_display_options(), 1);
$this->assertEqualsWithDelta(time(), $questionattempt->get_step(0)->get_timecreated(), 1);
}
}
@@ -694,4 +694,83 @@ final class attempt_walkthrough_test extends \advanced_testcase {
$this->assertEquals($attemptobj->get_context(), $gradedevent->get_context());
$this->assertEquals($attempturl, $gradedevent->get_url());
}
/**
* Create a quiz with questions, pre-create an attempt, edit a question, then begin the attempt.
*/
public function test_quiz_attempt_walkthrough_update_question_after_precreate(): void {
global $SITE;
$this->resetAfterTest(true);
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(
[
'course' => $SITE->id,
'questionsperpage' => 0,
'grade' => 100.0,
'sumgrades' => 3,
],
);
// Create a couple of questions.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
$numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
$matchq = $questiongenerator->create_question('match', null, ['category' => $cat->id]);
$description = $questiongenerator->create_question('description', null, ['category' => $cat->id]);
// Add them to the quiz.
quiz_add_quiz_question($saq->id, $quiz);
quiz_add_quiz_question($numq->id, $quiz);
quiz_add_quiz_question($matchq->id, $quiz);
quiz_add_quiz_question($description->id, $quiz);
// Make a user to do the quiz.
$user1 = $this->getDataGenerator()->create_user();
$quizobj = quiz_settings::create($quiz->id, $user1->id);
// Start the attempt.
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
$timenow = time();
$attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user1->id);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
$this->assertEquals('1,2,3,4,0', $attempt->layout);
quiz_attempt_save_not_started($quba, $attempt);
$attemptobj = quiz_attempt::create($attempt->id);
// Update question in quiz.
$newsa = $questiongenerator->update_question($saq, null,
['name' => 'This is the second version of shortanswer']);
$newnumbq = $questiongenerator->update_question($numq, null,
['name' => 'This is the second version of numerical']);
$newmatch = $questiongenerator->update_question($matchq, null,
['name' => 'This is the second version of match']);
$newdescription = $questiongenerator->update_question($description, null,
['name' => 'This is the second version of description']);
$this->assertEquals($saq->id, $attemptobj->get_question_attempt(1)->get_question_id());
$this->assertEquals($numq->id, $attemptobj->get_question_attempt(2)->get_question_id());
$this->assertEquals($matchq->id, $attemptobj->get_question_attempt(3)->get_question_id());
$this->assertEquals($description->id, $attemptobj->get_question_attempt(4)->get_question_id());
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Verify that the started attempt contains the new questions.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals($newsa->id, $attemptobj->get_question_attempt(1)->get_question_id());
$this->assertEquals($newnumbq->id, $attemptobj->get_question_attempt(2)->get_question_id());
$this->assertEquals($newmatch->id, $attemptobj->get_question_attempt(3)->get_question_id());
$this->assertEquals($newdescription->id, $attemptobj->get_question_attempt(4)->get_question_id());
}
}