Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
   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\external;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  require_once (__DIR__ . '/../../../../webservice/tests/helpers.php');
  22  
  23  use coding_exception;
  24  use core_question_generator;
  25  use externallib_advanced_testcase;
  26  use mod_quiz\quiz_attempt;
  27  use mod_quiz\quiz_settings;
  28  use required_capability_exception;
  29  use stdClass;
  30  
  31  /**
  32   * Test for the reopen_attempt and get_reopen_attempt_confirmation services.
  33   *
  34   * @package   mod_quiz
  35   * @category  external
  36   * @copyright 2023 The Open University
  37   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   * @covers \mod_quiz\external\reopen_attempt
  39   * @covers \mod_quiz\external\get_reopen_attempt_confirmation
  40   */
  41  class reopen_attempt_test extends externallib_advanced_testcase {
  42      /** @var stdClass|null if we make a quiz attempt, we store the student object here. */
  43      protected $student;
  44  
  45      public function test_reopen_attempt_service_works() {
  46          [$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
  47  
  48          reopen_attempt::execute($attemptid);
  49  
  50          $attemptobj = quiz_attempt::create($attemptid);
  51          $this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state());
  52      }
  53  
  54      public function test_reopen_attempt_service_checks_permissions() {
  55          [$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
  56  
  57          $unprivilegeduser = $this->getDataGenerator()->create_user();
  58          $this->setUser($unprivilegeduser);
  59  
  60          $this->expectException(required_capability_exception::class);
  61          reopen_attempt::execute($attemptid);
  62      }
  63  
  64      public function test_reopen_attempt_service_checks_attempt_state() {
  65          [$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question(quiz_attempt::IN_PROGRESS);
  66  
  67          $this->expectExceptionMessage("Attempt $attemptid is in the wrong state (In progress) to be reopened.");
  68          reopen_attempt::execute($attemptid);
  69      }
  70  
  71      public function test_get_reopen_attempt_confirmation_staying_open() {
  72          global $DB;
  73          [$attemptid, $quizid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
  74          $DB->set_field('quiz', 'timeclose', 0, ['id' => $quizid]);
  75  
  76          $message = get_reopen_attempt_confirmation::execute($attemptid);
  77  
  78          $this->assertEquals('<p>This will reopen attempt 1 by ' . fullname($this->student) .
  79                  '.</p><p>The attempt will remain open and can be continued.</p>',
  80                  $message);
  81      }
  82  
  83      public function test_get_reopen_attempt_confirmation_staying_open_until() {
  84          global $DB;
  85          [$attemptid, $quizid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
  86          $timeclose = time() + HOURSECS;
  87          $DB->set_field('quiz', 'timeclose', $timeclose, ['id' => $quizid]);
  88  
  89          $message = get_reopen_attempt_confirmation::execute($attemptid);
  90  
  91          $this->assertEquals('<p>This will reopen attempt 1 by ' . fullname($this->student) .
  92                  '.</p><p>The attempt will remain open and can be continued until the quiz closes on ' .
  93                  userdate($timeclose) . '.</p>',
  94                  $message);
  95      }
  96  
  97      public function test_get_reopen_attempt_confirmation_submitting() {
  98          global $DB;
  99          [$attemptid, $quizid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
 100          $timeclose = time() - HOURSECS;
 101          $DB->set_field('quiz', 'timeclose', $timeclose, ['id' => $quizid]);
 102  
 103          $message = get_reopen_attempt_confirmation::execute($attemptid);
 104  
 105          $this->assertEquals('<p>This will reopen attempt 1 by ' . fullname($this->student) .
 106                  '.</p><p>The attempt will be immediately submitted for grading.</p>',
 107                  $message);
 108      }
 109  
 110      public function test_get_reopen_attempt_confirmation_service_checks_permissions() {
 111          [$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
 112  
 113          $unprivilegeduser = $this->getDataGenerator()->create_user();
 114          $this->setUser($unprivilegeduser);
 115  
 116          $this->expectException(required_capability_exception::class);
 117          get_reopen_attempt_confirmation::execute($attemptid);
 118      }
 119  
 120      public function test_get_reopen_attempt_confirmation_service_checks_attempt_state() {
 121          [$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question(quiz_attempt::IN_PROGRESS);
 122  
 123          $this->expectExceptionMessage("Attempt $attemptid is in the wrong state (In progress) to be reopened.");
 124          get_reopen_attempt_confirmation::execute($attemptid);
 125      }
 126  
 127      /**
 128       * Create a quiz of one shortanswer question and an attempt in a given state.
 129       *
 130       * @param string $attemptstate the desired attempt state. quiz_attempt::ABANDONED or ::IN_PROGRESS.
 131       * @return array with two elements, the attempt id and the quiz id.
 132       */
 133      protected function create_attempt_at_quiz_with_one_shortanswer_question(
 134          string $attemptstate = quiz_attempt::ABANDONED
 135      ): array {
 136          global $SITE;
 137          $this->resetAfterTest();
 138  
 139          // Make a quiz.
 140          $timeclose = time() + HOURSECS;
 141          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 142  
 143          $quiz = $quizgenerator->create_instance([
 144              'course' => $SITE->id,
 145              'timeclose' => $timeclose,
 146              'overduehandling' => 'autoabandon'
 147          ]);
 148  
 149          // Create a question.
 150          /** @var core_question_generator $questiongenerator */
 151          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 152          $cat = $questiongenerator->create_question_category();
 153          $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 154  
 155          // Add them to the quiz.
 156          $quizobj = quiz_settings::create($quiz->id);
 157          quiz_add_quiz_question($saq->id, $quiz, 0, 1);
 158          $quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
 159  
 160          // Make a user to do the quiz.
 161          $this->student = $this->getDataGenerator()->create_user();
 162          $this->setUser($this->student);
 163          $quizobj = quiz_settings::create($quiz->id, $this->student->id);
 164  
 165          // Start the attempt.
 166          $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
 167          $attemptobj = quiz_attempt::create($attempt->id);
 168  
 169          if ($attemptstate === quiz_attempt::ABANDONED) {
 170              // Attempt goes overdue (e.g. if cron ran).
 171              $attemptobj->process_abandon($timeclose + 2 * get_config('quiz', 'graceperiodmin'), false);
 172          } else if ($attemptstate !== quiz_attempt::IN_PROGRESS) {
 173              throw new coding_exception('State ' . $attemptstate . ' not currently supported.');
 174          }
 175  
 176          // Set current user to admin before we return.
 177          $this->setAdminUser();
 178  
 179          return [$attemptobj->get_attemptid(), $attemptobj->get_quizid()];
 180      }
 181  }