Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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 question_bank;
  20  use question_engine;
  21  use quiz;
  22  use quiz_attempt;
  23  
  24  defined('MOODLE_INTERNAL') || die();
  25  
  26  global $CFG;
  27  require_once($CFG->dirroot . '/mod/quiz/locallib.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 \quiz_attempt
  38   */
  39  class attempt_walkthrough_test extends \advanced_testcase {
  40  
  41      /**
  42       * Create a quiz with questions and walk through a quiz attempt.
  43       */
  44      public function test_quiz_attempt_walkthrough() {
  45          global $SITE;
  46  
  47          $this->resetAfterTest(true);
  48  
  49          // Make a quiz.
  50          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
  51  
  52          $quiz = $quizgenerator->create_instance(array('course'=>$SITE->id, 'questionsperpage' => 0, 'grade' => 100.0,
  53                                                        'sumgrades' => 2));
  54  
  55          // Create a couple of questions.
  56          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
  57  
  58          $cat = $questiongenerator->create_question_category();
  59          $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
  60          $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
  61  
  62          // Add them to the quiz.
  63          quiz_add_quiz_question($saq->id, $quiz);
  64          quiz_add_quiz_question($numq->id, $quiz);
  65  
  66          // Make a user to do the quiz.
  67          $user1 = $this->getDataGenerator()->create_user();
  68  
  69          $quizobj = quiz::create($quiz->id, $user1->id);
  70  
  71          // Start the attempt.
  72          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
  73          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
  74  
  75          $timenow = time();
  76          $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user1->id);
  77  
  78          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
  79          $this->assertEquals('1,2,0', $attempt->layout);
  80  
  81          quiz_attempt_save_started($quizobj, $quba, $attempt);
  82  
  83          // Process some responses from the student.
  84          $attemptobj = quiz_attempt::create($attempt->id);
  85          $this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
  86  
  87          $tosubmit = array(1 => array('answer' => 'frog'),
  88                            2 => array('answer' => '3.14'));
  89  
  90          $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
  91  
  92          // Finish the attempt.
  93          $attemptobj = quiz_attempt::create($attempt->id);
  94          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
  95          $attemptobj->process_finish($timenow, false);
  96  
  97          // Re-load quiz attempt data.
  98          $attemptobj = quiz_attempt::create($attempt->id);
  99  
 100          // Check that results are stored as expected.
 101          $this->assertEquals(1, $attemptobj->get_attempt_number());
 102          $this->assertEquals(2, $attemptobj->get_sum_marks());
 103          $this->assertEquals(true, $attemptobj->is_finished());
 104          $this->assertEquals($timenow, $attemptobj->get_submitted_date());
 105          $this->assertEquals($user1->id, $attemptobj->get_userid());
 106          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 107  
 108          // Check quiz grades.
 109          $grades = quiz_get_user_grades($quiz, $user1->id);
 110          $grade = array_shift($grades);
 111          $this->assertEquals(100.0, $grade->rawgrade);
 112  
 113          // Check grade book.
 114          $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $quiz->id, $user1->id);
 115          $gradebookitem = array_shift($gradebookgrades->items);
 116          $gradebookgrade = array_shift($gradebookitem->grades);
 117          $this->assertEquals(100, $gradebookgrade->grade);
 118      }
 119  
 120      /**
 121       * Create a quiz containing one question and a close time.
 122       *
 123       * The question is the standard shortanswer test question.
 124       * The quiz is set to close 1 hour from now.
 125       * The quiz is set to use a grade period of 1 hour once time expires.
 126       *
 127       * @return \stdClass the quiz that was created.
 128       */
 129      protected function create_quiz_with_one_question(): \stdClass {
 130          global $SITE;
 131          $this->resetAfterTest();
 132  
 133          // Make a quiz.
 134          $timeclose = time() + HOURSECS;
 135          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 136  
 137          $quiz = $quizgenerator->create_instance(
 138                  ['course' => $SITE->id, 'timeclose' => $timeclose,
 139                          'overduehandling' => 'graceperiod', 'graceperiod' => HOURSECS]);
 140  
 141          // Create a question.
 142          /** @var \core_question_generator $questiongenerator */
 143          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 144          $cat = $questiongenerator->create_question_category();
 145          $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 146  
 147          // Add them to the quiz.
 148          quiz_add_quiz_question($saq->id, $quiz, 0, 1);
 149          quiz_update_sumgrades($quiz);
 150  
 151          return $quiz;
 152      }
 153  
 154      public function test_quiz_attempt_walkthrough_submit_time_recorded_correctly_when_overdue() {
 155  
 156          $quiz = $this->create_quiz_with_one_question();
 157  
 158          // Make a user to do the quiz.
 159          $user = $this->getDataGenerator()->create_user();
 160          $this->setUser($user);
 161          $quizobj = quiz::create($quiz->id, $user->id);
 162  
 163          // Start the attempt.
 164          $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
 165  
 166          // Process some responses from the student.
 167          $attemptobj = quiz_attempt::create($attempt->id);
 168          $attemptobj->process_submitted_actions($quiz->timeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
 169  
 170          // Attempt goes overdue (e.g. if cron ran).
 171          $attemptobj = quiz_attempt::create($attempt->id);
 172          $attemptobj->process_going_overdue($quiz->timeclose + 2 * get_config('quiz', 'graceperiodmin'), false);
 173  
 174          // Verify the attempt state.
 175          $attemptobj = quiz_attempt::create($attempt->id);
 176          $this->assertEquals(1, $attemptobj->get_attempt_number());
 177          $this->assertEquals(false, $attemptobj->is_finished());
 178          $this->assertEquals(0, $attemptobj->get_submitted_date());
 179          $this->assertEquals($user->id, $attemptobj->get_userid());
 180          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 181  
 182          // Student submits the attempt during the grace period.
 183          $attemptobj = quiz_attempt::create($attempt->id);
 184          $attemptobj->process_attempt($quiz->timeclose + 30 * MINSECS, true, false, 1);
 185  
 186          // Verify the attempt state.
 187          $attemptobj = quiz_attempt::create($attempt->id);
 188          $this->assertEquals(1, $attemptobj->get_attempt_number());
 189          $this->assertEquals(true, $attemptobj->is_finished());
 190          $this->assertEquals($quiz->timeclose + 30 * MINSECS, $attemptobj->get_submitted_date());
 191          $this->assertEquals($user->id, $attemptobj->get_userid());
 192          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 193      }
 194  
 195      public function test_quiz_attempt_walkthrough_close_time_extended_at_last_minute() {
 196          global $DB;
 197  
 198          $quiz = $this->create_quiz_with_one_question();
 199          $originaltimeclose = $quiz->timeclose;
 200  
 201          // Make a user to do the quiz.
 202          $user = $this->getDataGenerator()->create_user();
 203          $this->setUser($user);
 204          $quizobj = quiz::create($quiz->id, $user->id);
 205  
 206          // Start the attempt.
 207          $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
 208  
 209          // Process some responses from the student during the attempt.
 210          $attemptobj = quiz_attempt::create($attempt->id);
 211          $attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
 212  
 213          // Teacher edits the quiz to extend the time-limit by one minute.
 214          $DB->set_field('quiz', 'timeclose', $originaltimeclose + MINSECS, ['id' => $quiz->id]);
 215          \course_modinfo::clear_instance_cache($quiz->course);
 216  
 217          // Timer expires in the student browser and thinks it is time to submit the quiz.
 218          // This sets $finishattempt to false - since the student did not click the button, and $timeup to true.
 219          $attemptobj = quiz_attempt::create($attempt->id);
 220          $attemptobj->process_attempt($originaltimeclose, false, true, 1);
 221  
 222          // Verify the attempt state - the $timeup was ignored becuase things have changed server-side.
 223          $attemptobj = quiz_attempt::create($attempt->id);
 224          $this->assertEquals(1, $attemptobj->get_attempt_number());
 225          $this->assertFalse($attemptobj->is_finished());
 226          $this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state());
 227          $this->assertEquals(0, $attemptobj->get_submitted_date());
 228          $this->assertEquals($user->id, $attemptobj->get_userid());
 229      }
 230  
 231      /**
 232       * Create a quiz with a random as well as other questions and walk through quiz attempts.
 233       */
 234      public function test_quiz_with_random_question_attempt_walkthrough() {
 235          global $SITE;
 236  
 237          $this->resetAfterTest(true);
 238          question_bank::get_qtype('random')->clear_caches_before_testing();
 239  
 240          $this->setAdminUser();
 241  
 242          // Make a quiz.
 243          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 244  
 245          $quiz = $quizgenerator->create_instance(array('course' => $SITE->id, 'questionsperpage' => 2, 'grade' => 100.0,
 246                                                        'sumgrades' => 4));
 247  
 248          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 249  
 250          // Add two questions to question category.
 251          $cat = $questiongenerator->create_question_category();
 252          $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 253          $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
 254  
 255          // Add random question to the quiz.
 256          quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
 257  
 258          // Make another category.
 259          $cat2 = $questiongenerator->create_question_category();
 260          $match = $questiongenerator->create_question('match', null, array('category' => $cat->id));
 261  
 262          quiz_add_quiz_question($match->id, $quiz, 0);
 263  
 264          $multichoicemulti = $questiongenerator->create_question('multichoice', 'two_of_four', array('category' => $cat->id));
 265  
 266          quiz_add_quiz_question($multichoicemulti->id, $quiz, 0);
 267  
 268          $multichoicesingle = $questiongenerator->create_question('multichoice', 'one_of_four', array('category' => $cat->id));
 269  
 270          quiz_add_quiz_question($multichoicesingle->id, $quiz, 0);
 271  
 272          foreach (array($saq->id => 'frog', $numq->id => '3.14') as $randomqidtoselect => $randqanswer) {
 273              // Make a new user to do the quiz each loop.
 274              $user1 = $this->getDataGenerator()->create_user();
 275              $this->setUser($user1);
 276  
 277              $quizobj = quiz::create($quiz->id, $user1->id);
 278  
 279              // Start the attempt.
 280              $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 281              $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 282  
 283              $timenow = time();
 284              $attempt = quiz_create_attempt($quizobj, 1, false, $timenow);
 285  
 286              quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, array(1 => $randomqidtoselect));
 287              $this->assertEquals('1,2,0,3,4,0', $attempt->layout);
 288  
 289              quiz_attempt_save_started($quizobj, $quba, $attempt);
 290  
 291              // Process some responses from the student.
 292              $attemptobj = quiz_attempt::create($attempt->id);
 293              $this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
 294  
 295              $tosubmit = array();
 296              $selectedquestionid = $quba->get_question_attempt(1)->get_question_id();
 297              $tosubmit[1] = array('answer' => $randqanswer);
 298              $tosubmit[2] = array(
 299                  'frog' => 'amphibian',
 300                  'cat'  => 'mammal',
 301                  'newt' => 'amphibian');
 302              $tosubmit[3] = array('One' => '1', 'Two' => '0', 'Three' => '1', 'Four' => '0'); // First and third choice.
 303              $tosubmit[4] = array('answer' => 'One'); // The first choice.
 304  
 305              $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
 306  
 307              // Finish the attempt.
 308              $attemptobj = quiz_attempt::create($attempt->id);
 309              $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 310              $attemptobj->process_finish($timenow, false);
 311  
 312              // Re-load quiz attempt data.
 313              $attemptobj = quiz_attempt::create($attempt->id);
 314  
 315              // Check that results are stored as expected.
 316              $this->assertEquals(1, $attemptobj->get_attempt_number());
 317              $this->assertEquals(4, $attemptobj->get_sum_marks());
 318              $this->assertEquals(true, $attemptobj->is_finished());
 319              $this->assertEquals($timenow, $attemptobj->get_submitted_date());
 320              $this->assertEquals($user1->id, $attemptobj->get_userid());
 321              $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 322  
 323              // Check quiz grades.
 324              $grades = quiz_get_user_grades($quiz, $user1->id);
 325              $grade = array_shift($grades);
 326              $this->assertEquals(100.0, $grade->rawgrade);
 327  
 328              // Check grade book.
 329              $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $quiz->id, $user1->id);
 330              $gradebookitem = array_shift($gradebookgrades->items);
 331              $gradebookgrade = array_shift($gradebookitem->grades);
 332              $this->assertEquals(100, $gradebookgrade->grade);
 333          }
 334      }
 335  
 336  
 337      public function get_correct_response_for_variants() {
 338          return array(array(1, 9.9), array(2, 8.5), array(5, 14.2), array(10, 6.8, true));
 339      }
 340  
 341      protected $quizwithvariants = null;
 342  
 343      /**
 344       * Create a quiz with a single question with variants and walk through quiz attempts.
 345       *
 346       * @dataProvider get_correct_response_for_variants
 347       */
 348      public function test_quiz_with_question_with_variants_attempt_walkthrough($variantno, $correctresponse, $done = false) {
 349          global $SITE;
 350  
 351          $this->resetAfterTest($done);
 352  
 353          $this->setAdminUser();
 354  
 355          if ($this->quizwithvariants === null) {
 356              // Make a quiz.
 357              $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 358  
 359              $this->quizwithvariants = $quizgenerator->create_instance(array('course'=>$SITE->id,
 360                                                                              'questionsperpage' => 0,
 361                                                                              'grade' => 100.0,
 362                                                                              'sumgrades' => 1));
 363  
 364              $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 365  
 366              $cat = $questiongenerator->create_question_category();
 367              $calc = $questiongenerator->create_question('calculatedsimple', 'sumwithvariants', array('category' => $cat->id));
 368              quiz_add_quiz_question($calc->id, $this->quizwithvariants, 0);
 369          }
 370  
 371  
 372          // Make a new user to do the quiz.
 373          $user1 = $this->getDataGenerator()->create_user();
 374          $this->setUser($user1);
 375          $quizobj = quiz::create($this->quizwithvariants->id, $user1->id);
 376  
 377          // Start the attempt.
 378          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 379          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 380  
 381          $timenow = time();
 382          $attempt = quiz_create_attempt($quizobj, 1, false, $timenow);
 383  
 384          // Select variant.
 385          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, array(), array(1 => $variantno));
 386          $this->assertEquals('1,0', $attempt->layout);
 387          quiz_attempt_save_started($quizobj, $quba, $attempt);
 388  
 389          // Process some responses from the student.
 390          $attemptobj = quiz_attempt::create($attempt->id);
 391          $this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
 392  
 393          $tosubmit = array(1 => array('answer' => $correctresponse));
 394          $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
 395  
 396          // Finish the attempt.
 397          $attemptobj = quiz_attempt::create($attempt->id);
 398          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 399  
 400          $attemptobj->process_finish($timenow, false);
 401  
 402          // Re-load quiz attempt data.
 403          $attemptobj = quiz_attempt::create($attempt->id);
 404  
 405          // Check that results are stored as expected.
 406          $this->assertEquals(1, $attemptobj->get_attempt_number());
 407          $this->assertEquals(1, $attemptobj->get_sum_marks());
 408          $this->assertEquals(true, $attemptobj->is_finished());
 409          $this->assertEquals($timenow, $attemptobj->get_submitted_date());
 410          $this->assertEquals($user1->id, $attemptobj->get_userid());
 411          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 412  
 413          // Check quiz grades.
 414          $grades = quiz_get_user_grades($this->quizwithvariants, $user1->id);
 415          $grade = array_shift($grades);
 416          $this->assertEquals(100.0, $grade->rawgrade);
 417  
 418          // Check grade book.
 419          $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $this->quizwithvariants->id, $user1->id);
 420          $gradebookitem = array_shift($gradebookgrades->items);
 421          $gradebookgrade = array_shift($gradebookitem->grades);
 422          $this->assertEquals(100, $gradebookgrade->grade);
 423      }
 424  }