Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace mod_quiz;
  18  
  19  use moodle_url;
  20  use question_bank;
  21  use question_engine;
  22  
  23  defined('MOODLE_INTERNAL') || die();
  24  
  25  global $CFG;
  26  require_once($CFG->dirroot . '/mod/quiz/locallib.php');
  27  require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
  28  
  29  /**
  30   * Quiz attempt walk through.
  31   *
  32   * @package   mod_quiz
  33   * @category  test
  34   * @copyright 2013 The Open University
  35   * @author    Jamie Pratt <me@jamiep.org>
  36   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   * @covers \mod_quiz\quiz_attempt
  38   */
  39  class attempt_walkthrough_test extends \advanced_testcase {
  40  
  41      use \quiz_question_helper_test_trait;
  42  
  43      /**
  44       * Create a quiz with questions and walk through a quiz attempt.
  45       */
  46      public function test_quiz_attempt_walkthrough() {
  47          global $SITE;
  48  
  49          $this->resetAfterTest(true);
  50  
  51          // Make a quiz.
  52          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
  53  
  54          $quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 0, 'grade' => 100.0,
  55                                                        'sumgrades' => 3]);
  56  
  57          // Create a couple of questions.
  58          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
  59  
  60          $cat = $questiongenerator->create_question_category();
  61          $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
  62          $numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
  63          $matchq = $questiongenerator->create_question('match', null, ['category' => $cat->id]);
  64          $description = $questiongenerator->create_question('description', null, ['category' => $cat->id]);
  65  
  66          // Add them to the quiz.
  67          quiz_add_quiz_question($saq->id, $quiz);
  68          quiz_add_quiz_question($numq->id, $quiz);
  69          quiz_add_quiz_question($matchq->id, $quiz);
  70          quiz_add_quiz_question($description->id, $quiz);
  71  
  72          // Make a user to do the quiz.
  73          $user1 = $this->getDataGenerator()->create_user();
  74  
  75          $quizobj = quiz_settings::create($quiz->id, $user1->id);
  76  
  77          // Start the attempt.
  78          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
  79          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
  80  
  81          $timenow = time();
  82          $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user1->id);
  83  
  84          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
  85          $this->assertEquals('1,2,3,4,0', $attempt->layout);
  86  
  87          quiz_attempt_save_started($quizobj, $quba, $attempt);
  88  
  89          // Process some responses from the student.
  90          $attemptobj = quiz_attempt::create($attempt->id);
  91          $this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
  92          // The student has not answered any questions.
  93          $this->assertEquals(3, $attemptobj->get_number_of_unanswered_questions());
  94  
  95          $tosubmit = [1 => ['answer' => 'frog'],
  96                            2 => ['answer' => '3.14']];
  97  
  98          $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
  99          // The student has answered two questions, and only one remaining.
 100          $this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions());
 101  
 102          $tosubmit = [
 103              3 => [
 104                  'frog' => 'amphibian',
 105                  'cat' => 'mammal',
 106                  'newt' => ''
 107              ]
 108          ];
 109  
 110          $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
 111          // The student has answered three questions but one is invalid, so there is still one remaining.
 112          $this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions());
 113  
 114          $tosubmit = [
 115              3 => [
 116                  'frog' => 'amphibian',
 117                  'cat' => 'mammal',
 118                  'newt' => 'amphibian'
 119              ]
 120          ];
 121  
 122          $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
 123          // The student has answered three questions, so there are no remaining.
 124          $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
 125  
 126          // Finish the attempt.
 127          $attemptobj = quiz_attempt::create($attempt->id);
 128          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 129          $attemptobj->process_finish($timenow, false);
 130  
 131          // Re-load quiz attempt data.
 132          $attemptobj = quiz_attempt::create($attempt->id);
 133  
 134          // Check that results are stored as expected.
 135          $this->assertEquals(1, $attemptobj->get_attempt_number());
 136          $this->assertEquals(3, $attemptobj->get_sum_marks());
 137          $this->assertEquals(true, $attemptobj->is_finished());
 138          $this->assertEquals($timenow, $attemptobj->get_submitted_date());
 139          $this->assertEquals($user1->id, $attemptobj->get_userid());
 140          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 141          $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
 142  
 143          // Check quiz grades.
 144          $grades = quiz_get_user_grades($quiz, $user1->id);
 145          $grade = array_shift($grades);
 146          $this->assertEquals(100.0, $grade->rawgrade);
 147  
 148          // Check grade book.
 149          $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $quiz->id, $user1->id);
 150          $gradebookitem = array_shift($gradebookgrades->items);
 151          $gradebookgrade = array_shift($gradebookitem->grades);
 152          $this->assertEquals(100, $gradebookgrade->grade);
 153      }
 154  
 155      /**
 156       * Create a quiz containing one question and a close time.
 157       *
 158       * The question is the standard shortanswer test question.
 159       * The quiz is set to close 1 hour from now.
 160       * The quiz is set to use a grade period of 1 hour once time expires.
 161       *
 162       * @param string $overduehandling value for the overduehandling quiz setting.
 163       * @return \stdClass the quiz that was created.
 164       */
 165      protected function create_quiz_with_one_question(string $overduehandling = 'graceperiod'): \stdClass {
 166          global $SITE;
 167          $this->resetAfterTest();
 168  
 169          // Make a quiz.
 170          $timeclose = time() + HOURSECS;
 171          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 172  
 173          $quiz = $quizgenerator->create_instance(
 174                  ['course' => $SITE->id, 'timeclose' => $timeclose,
 175                          'overduehandling' => $overduehandling, 'graceperiod' => HOURSECS]);
 176  
 177          // Create a question.
 178          /** @var \core_question_generator $questiongenerator */
 179          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 180          $cat = $questiongenerator->create_question_category();
 181          $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 182  
 183          // Add them to the quiz.
 184          $quizobj = quiz_settings::create($quiz->id);
 185          quiz_add_quiz_question($saq->id, $quiz, 0, 1);
 186          $quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
 187  
 188          return $quiz;
 189      }
 190  
 191      public function test_quiz_attempt_walkthrough_submit_time_recorded_correctly_when_overdue() {
 192  
 193          $quiz = $this->create_quiz_with_one_question();
 194  
 195          // Make a user to do the quiz.
 196          $user = $this->getDataGenerator()->create_user();
 197          $this->setUser($user);
 198          $quizobj = quiz_settings::create($quiz->id, $user->id);
 199  
 200          // Start the attempt.
 201          $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
 202  
 203          // Process some responses from the student.
 204          $attemptobj = quiz_attempt::create($attempt->id);
 205          $this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions());
 206          $attemptobj->process_submitted_actions($quiz->timeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
 207  
 208          // Attempt goes overdue (e.g. if cron ran).
 209          $attemptobj = quiz_attempt::create($attempt->id);
 210          $attemptobj->process_going_overdue($quiz->timeclose + 2 * get_config('quiz', 'graceperiodmin'), false);
 211  
 212          // Verify the attempt state.
 213          $attemptobj = quiz_attempt::create($attempt->id);
 214          $this->assertEquals(1, $attemptobj->get_attempt_number());
 215          $this->assertEquals(false, $attemptobj->is_finished());
 216          $this->assertEquals(0, $attemptobj->get_submitted_date());
 217          $this->assertEquals($user->id, $attemptobj->get_userid());
 218          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 219          $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
 220  
 221          // Student submits the attempt during the grace period.
 222          $attemptobj = quiz_attempt::create($attempt->id);
 223          $attemptobj->process_attempt($quiz->timeclose + 30 * MINSECS, true, false, 1);
 224  
 225          // Verify the attempt state.
 226          $attemptobj = quiz_attempt::create($attempt->id);
 227          $this->assertEquals(1, $attemptobj->get_attempt_number());
 228          $this->assertEquals(true, $attemptobj->is_finished());
 229          $this->assertEquals($quiz->timeclose + 30 * MINSECS, $attemptobj->get_submitted_date());
 230          $this->assertEquals($user->id, $attemptobj->get_userid());
 231          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 232          $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
 233      }
 234  
 235      public function test_quiz_attempt_walkthrough_close_time_extended_at_last_minute() {
 236          global $DB;
 237  
 238          $quiz = $this->create_quiz_with_one_question();
 239          $originaltimeclose = $quiz->timeclose;
 240  
 241          // Make a user to do the quiz.
 242          $user = $this->getDataGenerator()->create_user();
 243          $this->setUser($user);
 244          $quizobj = quiz_settings::create($quiz->id, $user->id);
 245  
 246          // Start the attempt.
 247          $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
 248  
 249          // Process some responses from the student during the attempt.
 250          $attemptobj = quiz_attempt::create($attempt->id);
 251          $attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
 252  
 253          // Teacher edits the quiz to extend the time-limit by one minute.
 254          $DB->set_field('quiz', 'timeclose', $originaltimeclose + MINSECS, ['id' => $quiz->id]);
 255          \course_modinfo::clear_instance_cache($quiz->course);
 256  
 257          // Timer expires in the student browser and thinks it is time to submit the quiz.
 258          // This sets $finishattempt to false - since the student did not click the button, and $timeup to true.
 259          $attemptobj = quiz_attempt::create($attempt->id);
 260          $attemptobj->process_attempt($originaltimeclose, false, true, 1);
 261  
 262          // Verify the attempt state - the $timeup was ignored becuase things have changed server-side.
 263          $attemptobj = quiz_attempt::create($attempt->id);
 264          $this->assertEquals(1, $attemptobj->get_attempt_number());
 265          $this->assertFalse($attemptobj->is_finished());
 266          $this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state());
 267          $this->assertEquals(0, $attemptobj->get_submitted_date());
 268          $this->assertEquals($user->id, $attemptobj->get_userid());
 269      }
 270  
 271      /**
 272       * Create a quiz with a random as well as other questions and walk through quiz attempts.
 273       */
 274      public function test_quiz_with_random_question_attempt_walkthrough() {
 275          global $SITE;
 276  
 277          $this->resetAfterTest(true);
 278          question_bank::get_qtype('random')->clear_caches_before_testing();
 279  
 280          $this->setAdminUser();
 281  
 282          // Make a quiz.
 283          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 284  
 285          $quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 2, 'grade' => 100.0,
 286                                                        'sumgrades' => 4]);
 287  
 288          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 289  
 290          // Add two questions to question category.
 291          $cat = $questiongenerator->create_question_category();
 292          $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 293          $numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
 294  
 295          // Add random question to the quiz.
 296          $this->add_random_questions($quiz->id, 0, $cat->id, 1);
 297  
 298          // Make another category.
 299          $cat2 = $questiongenerator->create_question_category();
 300          $match = $questiongenerator->create_question('match', null, ['category' => $cat->id]);
 301  
 302          quiz_add_quiz_question($match->id, $quiz, 0);
 303  
 304          $multichoicemulti = $questiongenerator->create_question('multichoice', 'two_of_four', ['category' => $cat->id]);
 305  
 306          quiz_add_quiz_question($multichoicemulti->id, $quiz, 0);
 307  
 308          $multichoicesingle = $questiongenerator->create_question('multichoice', 'one_of_four', ['category' => $cat->id]);
 309  
 310          quiz_add_quiz_question($multichoicesingle->id, $quiz, 0);
 311  
 312          foreach ([$saq->id => 'frog', $numq->id => '3.14'] as $randomqidtoselect => $randqanswer) {
 313              // Make a new user to do the quiz each loop.
 314              $user1 = $this->getDataGenerator()->create_user();
 315              $this->setUser($user1);
 316  
 317              $quizobj = quiz_settings::create($quiz->id, $user1->id);
 318  
 319              // Start the attempt.
 320              $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 321              $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 322  
 323              $timenow = time();
 324              $attempt = quiz_create_attempt($quizobj, 1, false, $timenow);
 325  
 326              quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, [1 => $randomqidtoselect]);
 327              $this->assertEquals('1,2,0,3,4,0', $attempt->layout);
 328  
 329              quiz_attempt_save_started($quizobj, $quba, $attempt);
 330  
 331              // Process some responses from the student.
 332              $attemptobj = quiz_attempt::create($attempt->id);
 333              $this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
 334              $this->assertEquals(4, $attemptobj->get_number_of_unanswered_questions());
 335  
 336              $tosubmit = [];
 337              $selectedquestionid = $quba->get_question_attempt(1)->get_question_id();
 338              $tosubmit[1] = ['answer' => $randqanswer];
 339              $tosubmit[2] = [
 340                  'frog' => 'amphibian',
 341                  'cat'  => 'mammal',
 342                  'newt' => 'amphibian'];
 343              $tosubmit[3] = ['One' => '1', 'Two' => '0', 'Three' => '1', 'Four' => '0']; // First and third choice.
 344              $tosubmit[4] = ['answer' => 'One']; // The first choice.
 345  
 346              $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
 347  
 348              // Finish the attempt.
 349              $attemptobj = quiz_attempt::create($attempt->id);
 350              $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 351              $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
 352              $attemptobj->process_finish($timenow, false);
 353  
 354              // Re-load quiz attempt data.
 355              $attemptobj = quiz_attempt::create($attempt->id);
 356  
 357              // Check that results are stored as expected.
 358              $this->assertEquals(1, $attemptobj->get_attempt_number());
 359              $this->assertEquals(4, $attemptobj->get_sum_marks());
 360              $this->assertEquals(true, $attemptobj->is_finished());
 361              $this->assertEquals($timenow, $attemptobj->get_submitted_date());
 362              $this->assertEquals($user1->id, $attemptobj->get_userid());
 363              $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 364              $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
 365  
 366              // Check quiz grades.
 367              $grades = quiz_get_user_grades($quiz, $user1->id);
 368              $grade = array_shift($grades);
 369              $this->assertEquals(100.0, $grade->rawgrade);
 370  
 371              // Check grade book.
 372              $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $quiz->id, $user1->id);
 373              $gradebookitem = array_shift($gradebookgrades->items);
 374              $gradebookgrade = array_shift($gradebookitem->grades);
 375              $this->assertEquals(100, $gradebookgrade->grade);
 376          }
 377      }
 378  
 379  
 380      public function get_correct_response_for_variants() {
 381          return [[1, 9.9], [2, 8.5], [5, 14.2], [10, 6.8, true]];
 382      }
 383  
 384      protected $quizwithvariants = null;
 385  
 386      /**
 387       * Create a quiz with a single question with variants and walk through quiz attempts.
 388       *
 389       * @dataProvider get_correct_response_for_variants
 390       */
 391      public function test_quiz_with_question_with_variants_attempt_walkthrough($variantno, $correctresponse, $done = false) {
 392          global $SITE;
 393  
 394          $this->resetAfterTest($done);
 395  
 396          $this->setAdminUser();
 397  
 398          if ($this->quizwithvariants === null) {
 399              // Make a quiz.
 400              $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 401  
 402              $this->quizwithvariants = $quizgenerator->create_instance(['course' => $SITE->id,
 403                                                                              'questionsperpage' => 0,
 404                                                                              'grade' => 100.0,
 405                                                                              'sumgrades' => 1]);
 406  
 407              $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 408  
 409              $cat = $questiongenerator->create_question_category();
 410              $calc = $questiongenerator->create_question('calculatedsimple', 'sumwithvariants', ['category' => $cat->id]);
 411              quiz_add_quiz_question($calc->id, $this->quizwithvariants, 0);
 412          }
 413  
 414  
 415          // Make a new user to do the quiz.
 416          $user1 = $this->getDataGenerator()->create_user();
 417          $this->setUser($user1);
 418          $quizobj = quiz_settings::create($this->quizwithvariants->id, $user1->id);
 419  
 420          // Start the attempt.
 421          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 422          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 423  
 424          $timenow = time();
 425          $attempt = quiz_create_attempt($quizobj, 1, false, $timenow);
 426  
 427          // Select variant.
 428          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, [], [1 => $variantno]);
 429          $this->assertEquals('1,0', $attempt->layout);
 430          quiz_attempt_save_started($quizobj, $quba, $attempt);
 431  
 432          // Process some responses from the student.
 433          $attemptobj = quiz_attempt::create($attempt->id);
 434          $this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
 435          $this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions());
 436  
 437          $tosubmit = [1 => ['answer' => $correctresponse]];
 438          $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
 439  
 440          // Finish the attempt.
 441          $attemptobj = quiz_attempt::create($attempt->id);
 442          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 443          $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
 444  
 445          $attemptobj->process_finish($timenow, false);
 446  
 447          // Re-load quiz attempt data.
 448          $attemptobj = quiz_attempt::create($attempt->id);
 449  
 450          // Check that results are stored as expected.
 451          $this->assertEquals(1, $attemptobj->get_attempt_number());
 452          $this->assertEquals(1, $attemptobj->get_sum_marks());
 453          $this->assertEquals(true, $attemptobj->is_finished());
 454          $this->assertEquals($timenow, $attemptobj->get_submitted_date());
 455          $this->assertEquals($user1->id, $attemptobj->get_userid());
 456          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 457          $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
 458  
 459          // Check quiz grades.
 460          $grades = quiz_get_user_grades($this->quizwithvariants, $user1->id);
 461          $grade = array_shift($grades);
 462          $this->assertEquals(100.0, $grade->rawgrade);
 463  
 464          // Check grade book.
 465          $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $this->quizwithvariants->id, $user1->id);
 466          $gradebookitem = array_shift($gradebookgrades->items);
 467          $gradebookgrade = array_shift($gradebookitem->grades);
 468          $this->assertEquals(100, $gradebookgrade->grade);
 469      }
 470  
 471      public function test_quiz_attempt_walkthrough_abandoned_attempt_reopened_with_timelimit_override() {
 472          global $DB;
 473  
 474          $quiz = $this->create_quiz_with_one_question('autoabandon');
 475          $originaltimeclose = $quiz->timeclose;
 476  
 477          // Make a user to do the quiz.
 478          $user = $this->getDataGenerator()->create_user();
 479          $this->setUser($user);
 480          $quizobj = quiz_settings::create($quiz->id, $user->id);
 481  
 482          // Start the attempt.
 483          $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
 484  
 485          // Process some responses from the student during the attempt.
 486          $attemptobj = quiz_attempt::create($attempt->id);
 487          $attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
 488  
 489          // Student leaves, so cron closes the attempt when time expires.
 490          $attemptobj->process_abandon($originaltimeclose + 5 * MINSECS, false);
 491  
 492          // Verify the attempt state.
 493          $attemptobj = quiz_attempt::create($attempt->id);
 494          $this->assertEquals(quiz_attempt::ABANDONED, $attemptobj->get_state());
 495          $this->assertEquals(0, $attemptobj->get_submitted_date());
 496          $this->assertEquals($user->id, $attemptobj->get_userid());
 497  
 498          // The teacher feels kind, so adds an override for the student, and re-opens the attempt.
 499          $sink = $this->redirectEvents();
 500          $overriddentimeclose = $originaltimeclose + HOURSECS;
 501          $DB->insert_record('quiz_overrides', [
 502              'quiz' => $quiz->id,
 503              'userid' => $user->id,
 504              'timeclose' => $overriddentimeclose,
 505          ]);
 506          $attemptobj = quiz_attempt::create($attempt->id);
 507          $reopentime = $originaltimeclose + 10 * MINSECS;
 508          $attemptobj->process_reopen_abandoned($reopentime);
 509  
 510          // Verify the attempt state.
 511          $attemptobj = quiz_attempt::create($attempt->id);
 512          $this->assertEquals(1, $attemptobj->get_attempt_number());
 513          $this->assertFalse($attemptobj->is_finished());
 514          $this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state());
 515          $this->assertEquals(0, $attemptobj->get_submitted_date());
 516          $this->assertEquals($user->id, $attemptobj->get_userid());
 517          $this->assertEquals($overriddentimeclose,
 518                  $attemptobj->get_access_manager($reopentime)->get_end_time($attemptobj->get_attempt()));
 519  
 520          // Verify this was logged correctly.
 521          $events = $sink->get_events();
 522          $this->assertCount(1, $events);
 523  
 524          $reopenedevent = array_shift($events);
 525          $this->assertInstanceOf('\mod_quiz\event\attempt_reopened', $reopenedevent);
 526          $this->assertEquals($attemptobj->get_context(), $reopenedevent->get_context());
 527          $this->assertEquals(new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptobj->get_attemptid()]),
 528                  $reopenedevent->get_url());
 529      }
 530  
 531      public function test_quiz_attempt_walkthrough_abandoned_attempt_reopened_after_close_time() {
 532          $quiz = $this->create_quiz_with_one_question('autoabandon');
 533          $originaltimeclose = $quiz->timeclose;
 534  
 535          // Make a user to do the quiz.
 536          $user = $this->getDataGenerator()->create_user();
 537          $this->setUser($user);
 538          $quizobj = quiz_settings::create($quiz->id, $user->id);
 539  
 540          // Start the attempt.
 541          $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
 542  
 543          // Process some responses from the student during the attempt.
 544          $attemptobj = quiz_attempt::create($attempt->id);
 545          $attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
 546  
 547          // Student leaves, so cron closes the attempt when time expires.
 548          $attemptobj->process_abandon($originaltimeclose + 5 * MINSECS, false);
 549  
 550          // Verify the attempt state.
 551          $attemptobj = quiz_attempt::create($attempt->id);
 552          $this->assertEquals(quiz_attempt::ABANDONED, $attemptobj->get_state());
 553          $this->assertEquals(0, $attemptobj->get_submitted_date());
 554          $this->assertEquals($user->id, $attemptobj->get_userid());
 555  
 556          // The teacher reopens the attempt without granting more time, so previously submitted responess are graded.
 557          $sink = $this->redirectEvents();
 558          $reopentime = $originaltimeclose + 10 * MINSECS;
 559          $attemptobj->process_reopen_abandoned($reopentime);
 560  
 561          // Verify the attempt state.
 562          $attemptobj = quiz_attempt::create($attempt->id);
 563          $this->assertEquals(1, $attemptobj->get_attempt_number());
 564          $this->assertTrue($attemptobj->is_finished());
 565          $this->assertEquals(quiz_attempt::FINISHED, $attemptobj->get_state());
 566          $this->assertEquals($originaltimeclose, $attemptobj->get_submitted_date());
 567          $this->assertEquals($user->id, $attemptobj->get_userid());
 568          $this->assertEquals(1, $attemptobj->get_sum_marks());
 569  
 570          // Verify this was logged correctly - there are some gradebook events between the two we want to check.
 571          $events = $sink->get_events();
 572          $this->assertGreaterThanOrEqual(2, $events);
 573  
 574          $reopenedevent = array_shift($events);
 575          $this->assertInstanceOf('\mod_quiz\event\attempt_reopened', $reopenedevent);
 576          $this->assertEquals($attemptobj->get_context(), $reopenedevent->get_context());
 577          $this->assertEquals(new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptobj->get_attemptid()]),
 578                  $reopenedevent->get_url());
 579  
 580          $submittedevent = array_pop($events);
 581          $this->assertInstanceOf('\mod_quiz\event\attempt_submitted', $submittedevent);
 582          $this->assertEquals($attemptobj->get_context(), $submittedevent->get_context());
 583          $this->assertEquals(new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptobj->get_attemptid()]),
 584                  $submittedevent->get_url());
 585      }
 586  }