diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 94219795a0f..1c1aabb0375 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -2386,28 +2386,32 @@ class quiz_attempt { $transaction = $DB->start_delegated_transaction(); - // If there is only a very small amount of time left, there is no point trying - // to show the student another page of the quiz. Just finish now. - $graceperiodmin = null; + // Get key times. $accessmanager = $this->get_access_manager($timenow); $timeclose = $accessmanager->get_end_time($this->get_attempt()); + $graceperiodmin = get_config('quiz', 'graceperiodmin'); // Don't enforce timeclose for previews. if ($this->is_preview()) { $timeclose = false; } - $toolate = false; - if ($timeclose !== false && $timenow > $timeclose - QUIZ_MIN_TIME_TO_CONTINUE) { - $timeup = true; - $graceperiodmin = get_config('quiz', 'graceperiodmin'); - if ($timenow > $timeclose + $graceperiodmin) { - $toolate = true; - } - } - // Verify if time is really up; time limit could have been changed. - if ($timeup && $timenow < $timeclose - QUIZ_MIN_TIME_TO_CONTINUE) { - $timeup = false; + // Check where we are in relation to the end time, if there is one. + $toolate = false; + if ($timeclose !== false) { + if ($timenow > $timeclose - QUIZ_MIN_TIME_TO_CONTINUE) { + // If there is only a very small amount of time left, there is no point trying + // to show the student another page of the quiz. Just finish now. + $timeup = true; + if ($timenow > $timeclose + $graceperiodmin) { + $toolate = true; + } + } else { + // If time is not close to expiring, then ignore the client-side timer's opinion + // about whether time has expired. This can happen if the time limit has changed + // since the student's previous interaction. + $timeup = false; + } } // If time is running out, trigger the appropriate action. @@ -2415,9 +2419,6 @@ class quiz_attempt { $becomingabandoned = false; if ($timeup) { if ($this->get_quiz()->overduehandling === 'graceperiod') { - if (is_null($graceperiodmin)) { - $graceperiodmin = get_config('quiz', 'graceperiodmin'); - } if ($timenow > $timeclose + $this->get_quiz()->graceperiod + $graceperiodmin) { // Grace period has run out. $finishattempt = true; diff --git a/mod/quiz/tests/attempt_walkthrough_test.php b/mod/quiz/tests/attempt_walkthrough_test.php index ea26feda1b7..542d01b3fe4 100644 --- a/mod/quiz/tests/attempt_walkthrough_test.php +++ b/mod/quiz/tests/attempt_walkthrough_test.php @@ -34,6 +34,7 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php'); * @copyright 2013 The Open University * @author Jamie Pratt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \quiz_attempt */ class attempt_walkthrough_test extends \advanced_testcase { @@ -116,9 +117,17 @@ class attempt_walkthrough_test extends \advanced_testcase { $this->assertEquals(100, $gradebookgrade->grade); } - public function test_quiz_attempt_walkthrough_submit_time_recorded_correctly_when_overdue() { + /** + * Create a quiz containing one question and a close time. + * + * The question is the standard shortanswer test question. + * The quiz is set to close 1 hour from now. + * The quiz is set to use a grade period of 1 hour once time expires. + * + * @return \stdClass the quiz that was created. + */ + protected function create_quiz_with_one_question(): \stdClass { global $SITE; - $this->resetAfterTest(); // Make a quiz. @@ -130,6 +139,7 @@ class attempt_walkthrough_test extends \advanced_testcase { 'overduehandling' => 'graceperiod', 'graceperiod' => HOURSECS]); // Create a question. + /** @var \core_question_generator $questiongenerator */ $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $questiongenerator->create_question_category(); $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); @@ -138,6 +148,13 @@ class attempt_walkthrough_test extends \advanced_testcase { quiz_add_quiz_question($saq->id, $quiz, 0, 1); quiz_update_sumgrades($quiz); + return $quiz; + } + + public function test_quiz_attempt_walkthrough_submit_time_recorded_correctly_when_overdue() { + + $quiz = $this->create_quiz_with_one_question(); + // Make a user to do the quiz. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); @@ -148,11 +165,11 @@ class attempt_walkthrough_test extends \advanced_testcase { // Process some responses from the student. $attemptobj = quiz_attempt::create($attempt->id); - $attemptobj->process_submitted_actions($timeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]); + $attemptobj->process_submitted_actions($quiz->timeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]); // Attempt goes overdue (e.g. if cron ran). $attemptobj = quiz_attempt::create($attempt->id); - $attemptobj->process_going_overdue($timeclose + 2 * get_config('quiz', 'graceperiodmin'), false); + $attemptobj->process_going_overdue($quiz->timeclose + 2 * get_config('quiz', 'graceperiodmin'), false); // Verify the attempt state. $attemptobj = quiz_attempt::create($attempt->id); @@ -164,17 +181,53 @@ class attempt_walkthrough_test extends \advanced_testcase { // Student submits the attempt during the grace period. $attemptobj = quiz_attempt::create($attempt->id); - $attemptobj->process_attempt($timeclose + 30 * MINSECS, true, false, 1); + $attemptobj->process_attempt($quiz->timeclose + 30 * MINSECS, true, false, 1); // Verify the attempt state. $attemptobj = quiz_attempt::create($attempt->id); $this->assertEquals(1, $attemptobj->get_attempt_number()); $this->assertEquals(true, $attemptobj->is_finished()); - $this->assertEquals($timeclose + 30 * MINSECS, $attemptobj->get_submitted_date()); + $this->assertEquals($quiz->timeclose + 30 * MINSECS, $attemptobj->get_submitted_date()); $this->assertEquals($user->id, $attemptobj->get_userid()); $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); } + public function test_quiz_attempt_walkthrough_close_time_extended_at_last_minute() { + global $DB; + + $quiz = $this->create_quiz_with_one_question(); + $originaltimeclose = $quiz->timeclose; + + // Make a user to do the quiz. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + $quizobj = quiz::create($quiz->id, $user->id); + + // Start the attempt. + $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null); + + // Process some responses from the student during the attempt. + $attemptobj = quiz_attempt::create($attempt->id); + $attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]); + + // Teacher edits the quiz to extend the time-limit by one minute. + $DB->set_field('quiz', 'timeclose', $originaltimeclose + MINSECS, ['id' => $quiz->id]); + \course_modinfo::clear_instance_cache($quiz->course); + + // Timer expires in the student browser and thinks it is time to submit the quiz. + // This sets $finishattempt to false - since the student did not click the button, and $timeup to true. + $attemptobj = quiz_attempt::create($attempt->id); + $attemptobj->process_attempt($originaltimeclose, false, true, 1); + + // Verify the attempt state - the $timeup was ignored becuase things have changed server-side. + $attemptobj = quiz_attempt::create($attempt->id); + $this->assertEquals(1, $attemptobj->get_attempt_number()); + $this->assertFalse($attemptobj->is_finished()); + $this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state()); + $this->assertEquals(0, $attemptobj->get_submitted_date()); + $this->assertEquals($user->id, $attemptobj->get_userid()); + } + /** * Create a quiz with a random as well as other questions and walk through quiz attempts. */