Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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  /**
  18   * Quiz module external functions tests.
  19   *
  20   * @package    mod_quiz
  21   * @category   external
  22   * @copyright  2016 Juan Leyva <juan@moodle.com>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   * @since      Moodle 3.1
  25   */
  26  
  27  namespace mod_quiz\external;
  28  
  29  use externallib_advanced_testcase;
  30  use mod_quiz_external;
  31  use mod_quiz_display_options;
  32  use question_usage_by_activity;
  33  use quiz;
  34  use quiz_attempt;
  35  
  36  defined('MOODLE_INTERNAL') || die();
  37  
  38  global $CFG;
  39  
  40  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  41  
  42  /**
  43   * Silly class to access mod_quiz_external internal methods.
  44   *
  45   * @package mod_quiz
  46   * @copyright 2016 Juan Leyva <juan@moodle.com>
  47   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  48   * @since  Moodle 3.1
  49   */
  50  class testable_mod_quiz_external extends mod_quiz_external {
  51  
  52      /**
  53       * Public accessor.
  54       *
  55       * @param  array $params Array of parameters including the attemptid and preflight data
  56       * @param  bool $checkaccessrules whether to check the quiz access rules or not
  57       * @param  bool $failifoverdue whether to return error if the attempt is overdue
  58       * @return  array containing the attempt object and access messages
  59       */
  60      public static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
  61          return parent::validate_attempt($params, $checkaccessrules, $failifoverdue);
  62      }
  63  
  64      /**
  65       * Public accessor.
  66       *
  67       * @param  array $params Array of parameters including the attemptid
  68       * @return  array containing the attempt object and display options
  69       */
  70      public static function validate_attempt_review($params) {
  71          return parent::validate_attempt_review($params);
  72      }
  73  }
  74  
  75  /**
  76   * Quiz module external functions tests
  77   *
  78   * @package    mod_quiz
  79   * @category   external
  80   * @copyright  2016 Juan Leyva <juan@moodle.com>
  81   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  82   * @since      Moodle 3.1
  83   */
  84  class external_test extends externallib_advanced_testcase {
  85  
  86      /**
  87       * Set up for every test
  88       */
  89      public function setUp(): void {
  90          global $DB;
  91          $this->resetAfterTest();
  92          $this->setAdminUser();
  93  
  94          // Setup test data.
  95          $this->course = $this->getDataGenerator()->create_course();
  96          $this->quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $this->course->id));
  97          $this->context = \context_module::instance($this->quiz->cmid);
  98          $this->cm = get_coursemodule_from_instance('quiz', $this->quiz->id);
  99  
 100          // Create users.
 101          $this->student = self::getDataGenerator()->create_user();
 102          $this->teacher = self::getDataGenerator()->create_user();
 103  
 104          // Users enrolments.
 105          $this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
 106          $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
 107          // Allow student to receive messages.
 108          $coursecontext = \context_course::instance($this->course->id);
 109          assign_capability('mod/quiz:emailnotifysubmission', CAP_ALLOW, $this->teacherrole->id, $coursecontext, true);
 110  
 111          $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
 112          $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
 113      }
 114  
 115      /**
 116       * Test that a sequential navigation quiz is not allowing to see questions in advance except if reviewing
 117       */
 118      public function test_sequential_navigation_view_attempt() {
 119          // Test user with full capabilities.
 120          $quiz = $this->prepare_sequential_quiz();
 121          $attemptobj = $this->create_quiz_attempt_object($quiz);
 122          $this->setUser($this->student);
 123          // Check out of sequence access for view.
 124          $this->assertNotEmpty(mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 0, []));
 125          try {
 126              mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 3, []);
 127              $this->fail('Exception expected due to out of sequence access.');
 128          } catch (\moodle_exception $e) {
 129              $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
 130          }
 131      }
 132  
 133      /**
 134       * Test that a sequential navigation quiz is not allowing to see questions in advance for a student
 135       */
 136      public function test_sequential_navigation_attempt_summary() {
 137          // Test user with full capabilities.
 138          $quiz = $this->prepare_sequential_quiz();
 139          $attemptobj = $this->create_quiz_attempt_object($quiz);
 140          $this->setUser($this->student);
 141          // Check that we do not return other questions than the one currently viewed.
 142          $result = mod_quiz_external::get_attempt_summary($attemptobj->get_attemptid());
 143          $this->assertCount(1, $result['questions']);
 144          $this->assertStringContainsString('Question (1)', $result['questions'][0]['html']);
 145      }
 146  
 147      /**
 148       * Test that a sequential navigation quiz is not allowing to see questions in advance for student
 149       */
 150      public function test_sequential_navigation_get_attempt_data() {
 151          // Test user with full capabilities.
 152          $quiz = $this->prepare_sequential_quiz();
 153          $attemptobj = $this->create_quiz_attempt_object($quiz);
 154          $this->setUser($this->student);
 155          // Test invalid instance id.
 156          try {
 157              mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 2);
 158              $this->fail('Exception expected due to out of sequence access.');
 159          } catch (\moodle_exception $e) {
 160              $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
 161          }
 162          // Now we moved to page 1, we should see page 2 and 1 but not 0 or 3.
 163          $attemptobj->set_currentpage(1);
 164          // Test invalid instance id.
 165          try {
 166              mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 0);
 167              $this->fail('Exception expected due to out of sequence access.');
 168          } catch (\moodle_exception $e) {
 169              $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
 170          }
 171  
 172          try {
 173              mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 3);
 174              $this->fail('Exception expected due to out of sequence access.');
 175          } catch (\moodle_exception $e) {
 176              $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
 177          }
 178  
 179          // Now we can see page 1.
 180          $result = mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 1);
 181          $this->assertCount(1, $result['questions']);
 182          $this->assertStringContainsString('Question (2)', $result['questions'][0]['html']);
 183      }
 184  
 185      /**
 186       * Prepare quiz for sequential navigation tests
 187       *
 188       * @return quiz
 189       */
 190      private function prepare_sequential_quiz() {
 191          // Create a new quiz with 5 questions and one attempt started.
 192          // Create a new quiz with attempts.
 193          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 194          $data = [
 195              'course' => $this->course->id,
 196              'sumgrades' => 2,
 197              'preferredbehaviour' => 'deferredfeedback',
 198              'navmethod' => QUIZ_NAVMETHOD_SEQ
 199          ];
 200          $quiz = $quizgenerator->create_instance($data);
 201  
 202          // Now generate the questions.
 203          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 204          $cat = $questiongenerator->create_question_category();
 205          for ($pageindex = 1; $pageindex <= 5; $pageindex++) {
 206              $question = $questiongenerator->create_question('truefalse', null, [
 207                  'category' => $cat->id,
 208                  'questiontext' => ['text' => "Question ($pageindex)"]
 209              ]);
 210              quiz_add_quiz_question($question->id, $quiz, $pageindex);
 211          }
 212  
 213          $quizobj = quiz::create($quiz->id, $this->student->id);
 214          // Set grade to pass.
 215          $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
 216              'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
 217          $item->gradepass = 80;
 218          $item->update();
 219          return $quizobj;
 220      }
 221  
 222      /**
 223       * Create question attempt
 224       *
 225       * @param quiz $quizobj
 226       * @param int|null $userid
 227       * @param bool|null $ispreview
 228       * @return quiz_attempt
 229       * @throws \moodle_exception
 230       */
 231      private function create_quiz_attempt_object($quizobj, $userid = null, $ispreview = false) {
 232          global $USER;
 233          $timenow = time();
 234          // Now, do one attempt.
 235          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 236          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 237          $attemptnumber = 1;
 238          if (!empty($USER->id)) {
 239              $attemptnumber = count(quiz_get_user_attempts($quizobj->get_quizid(), $USER->id)) + 1;
 240          }
 241          $attempt = quiz_create_attempt($quizobj, $attemptnumber, false, $timenow, $ispreview, $userid ?? $this->student->id);
 242          quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow);
 243          quiz_attempt_save_started($quizobj, $quba, $attempt);
 244          $attemptobj = quiz_attempt::create($attempt->id);
 245          return $attemptobj;
 246      }
 247  }