Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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 quiz;
  33  use quiz_attempt;
  34  
  35  defined('MOODLE_INTERNAL') || die();
  36  
  37  global $CFG;
  38  
  39  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  40  
  41  /**
  42   * Silly class to access mod_quiz_external internal methods.
  43   *
  44   * @package mod_quiz
  45   * @copyright 2016 Juan Leyva <juan@moodle.com>
  46   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  47   * @since  Moodle 3.1
  48   */
  49  class testable_mod_quiz_external extends mod_quiz_external {
  50  
  51      /**
  52       * Public accessor.
  53       *
  54       * @param  array $params Array of parameters including the attemptid and preflight data
  55       * @param  bool $checkaccessrules whether to check the quiz access rules or not
  56       * @param  bool $failifoverdue whether to return error if the attempt is overdue
  57       * @return  array containing the attempt object and access messages
  58       */
  59      public static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
  60          return parent::validate_attempt($params, $checkaccessrules, $failifoverdue);
  61      }
  62  
  63      /**
  64       * Public accessor.
  65       *
  66       * @param  array $params Array of parameters including the attemptid
  67       * @return  array containing the attempt object and display options
  68       */
  69      public static function validate_attempt_review($params) {
  70          return parent::validate_attempt_review($params);
  71      }
  72  }
  73  
  74  /**
  75   * Quiz module external functions tests
  76   *
  77   * @package    mod_quiz
  78   * @category   external
  79   * @copyright  2016 Juan Leyva <juan@moodle.com>
  80   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  81   * @since      Moodle 3.1
  82   */
  83  class external_test extends externallib_advanced_testcase {
  84  
  85      /**
  86       * Set up for every test
  87       */
  88      public function setUp(): void {
  89          global $DB;
  90          $this->resetAfterTest();
  91          $this->setAdminUser();
  92  
  93          // Setup test data.
  94          $this->course = $this->getDataGenerator()->create_course();
  95          $this->quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $this->course->id));
  96          $this->context = \context_module::instance($this->quiz->cmid);
  97          $this->cm = get_coursemodule_from_instance('quiz', $this->quiz->id);
  98  
  99          // Create users.
 100          $this->student = self::getDataGenerator()->create_user();
 101          $this->teacher = self::getDataGenerator()->create_user();
 102  
 103          // Users enrolments.
 104          $this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
 105          $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
 106          // Allow student to receive messages.
 107          $coursecontext = \context_course::instance($this->course->id);
 108          assign_capability('mod/quiz:emailnotifysubmission', CAP_ALLOW, $this->teacherrole->id, $coursecontext, true);
 109  
 110          $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
 111          $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
 112      }
 113  
 114      /**
 115       * Create a quiz with questions including a started or finished attempt optionally
 116       *
 117       * @param  boolean $startattempt whether to start a new attempt
 118       * @param  boolean $finishattempt whether to finish the new attempt
 119       * @param  string $behaviour the quiz preferredbehaviour, defaults to 'deferredfeedback'.
 120       * @param  boolean $includeqattachments whether to include a question that supports attachments, defaults to false.
 121       * @param  array $extraoptions extra options for Quiz.
 122       * @return array array containing the quiz, context and the attempt
 123       */
 124      private function create_quiz_with_questions($startattempt = false, $finishattempt = false, $behaviour = 'deferredfeedback',
 125              $includeqattachments = false, $extraoptions = []) {
 126  
 127          // Create a new quiz with attempts.
 128          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 129          $data = array('course' => $this->course->id,
 130                        'sumgrades' => 2,
 131                        'preferredbehaviour' => $behaviour);
 132          $data = array_merge($data, $extraoptions);
 133          $quiz = $quizgenerator->create_instance($data);
 134          $context = \context_module::instance($quiz->cmid);
 135  
 136          // Create a couple of questions.
 137          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 138  
 139          $cat = $questiongenerator->create_question_category();
 140          $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
 141          quiz_add_quiz_question($question->id, $quiz);
 142          $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
 143          quiz_add_quiz_question($question->id, $quiz);
 144  
 145          if ($includeqattachments) {
 146              $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id, 'attachments' => 1,
 147                  'attachmentsrequired' => 1));
 148              quiz_add_quiz_question($question->id, $quiz);
 149          }
 150  
 151          $quizobj = quiz::create($quiz->id, $this->student->id);
 152  
 153          // Set grade to pass.
 154          $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
 155                                          'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
 156          $item->gradepass = 80;
 157          $item->update();
 158  
 159          if ($startattempt or $finishattempt) {
 160              // Now, do one attempt.
 161              $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 162              $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 163  
 164              $timenow = time();
 165              $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
 166              quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 167              quiz_attempt_save_started($quizobj, $quba, $attempt);
 168              $attemptobj = quiz_attempt::create($attempt->id);
 169  
 170              if ($finishattempt) {
 171                  // Process some responses from the student.
 172                  $tosubmit = array(1 => array('answer' => '3.14'));
 173                  $attemptobj->process_submitted_actions(time(), false, $tosubmit);
 174  
 175                  // Finish the attempt.
 176                  $attemptobj->process_finish(time(), false);
 177              }
 178              return array($quiz, $context, $quizobj, $attempt, $attemptobj, $quba);
 179          } else {
 180              return array($quiz, $context, $quizobj);
 181          }
 182  
 183      }
 184  
 185      /*
 186       * Test get quizzes by courses
 187       */
 188      public function test_mod_quiz_get_quizzes_by_courses() {
 189          global $DB;
 190  
 191          // Create additional course.
 192          $course2 = self::getDataGenerator()->create_course();
 193  
 194          // Second quiz.
 195          $record = new \stdClass();
 196          $record->course = $course2->id;
 197          $record->intro = '<button>Test with HTML allowed.</button>';
 198          $quiz2 = self::getDataGenerator()->create_module('quiz', $record);
 199  
 200          // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
 201          $enrol = enrol_get_plugin('manual');
 202          $enrolinstances = enrol_get_instances($course2->id, true);
 203          foreach ($enrolinstances as $courseenrolinstance) {
 204              if ($courseenrolinstance->enrol == "manual") {
 205                  $instance2 = $courseenrolinstance;
 206                  break;
 207              }
 208          }
 209          $enrol->enrol_user($instance2, $this->student->id, $this->studentrole->id);
 210  
 211          self::setUser($this->student);
 212  
 213          $returndescription = mod_quiz_external::get_quizzes_by_courses_returns();
 214  
 215          // Create what we expect to be returned when querying the two courses.
 216          // First for the student user.
 217          $allusersfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'timeopen',
 218                                  'timeclose', 'grademethod', 'section', 'visible', 'groupmode', 'groupingid',
 219                                  'attempts', 'timelimit', 'grademethod', 'decimalpoints', 'questiondecimalpoints', 'sumgrades',
 220                                  'grade', 'preferredbehaviour', 'hasfeedback');
 221          $userswithaccessfields = array('attemptonlast', 'reviewattempt', 'reviewcorrectness', 'reviewmarks',
 222                                          'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer',
 223                                          'reviewoverallfeedback', 'questionsperpage', 'navmethod',
 224                                          'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks',
 225                                          'completionattemptsexhausted', 'completionpass', 'autosaveperiod', 'hasquestions',
 226                                          'overduehandling', 'graceperiod', 'canredoquestions', 'allowofflineattempts');
 227          $managerfields = array('shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet');
 228  
 229          // Add expected coursemodule and other data.
 230          $quiz1 = $this->quiz;
 231          $quiz1->coursemodule = $quiz1->cmid;
 232          $quiz1->introformat = 1;
 233          $quiz1->section = 0;
 234          $quiz1->visible = true;
 235          $quiz1->groupmode = 0;
 236          $quiz1->groupingid = 0;
 237          $quiz1->hasquestions = 0;
 238          $quiz1->hasfeedback = 0;
 239          $quiz1->autosaveperiod = get_config('quiz', 'autosaveperiod');
 240          $quiz1->introfiles = [];
 241  
 242          $quiz2->coursemodule = $quiz2->cmid;
 243          $quiz2->introformat = 1;
 244          $quiz2->section = 0;
 245          $quiz2->visible = true;
 246          $quiz2->groupmode = 0;
 247          $quiz2->groupingid = 0;
 248          $quiz2->hasquestions = 0;
 249          $quiz2->hasfeedback = 0;
 250          $quiz2->autosaveperiod = get_config('quiz', 'autosaveperiod');
 251          $quiz2->introfiles = [];
 252  
 253          foreach (array_merge($allusersfields, $userswithaccessfields) as $field) {
 254              $expected1[$field] = $quiz1->{$field};
 255              $expected2[$field] = $quiz2->{$field};
 256          }
 257  
 258          $expectedquizzes = array($expected2, $expected1);
 259  
 260          // Call the external function passing course ids.
 261          $result = mod_quiz_external::get_quizzes_by_courses(array($course2->id, $this->course->id));
 262          $result = \external_api::clean_returnvalue($returndescription, $result);
 263  
 264          $this->assertEquals($expectedquizzes, $result['quizzes']);
 265          $this->assertCount(0, $result['warnings']);
 266  
 267          // Call the external function without passing course id.
 268          $result = mod_quiz_external::get_quizzes_by_courses();
 269          $result = \external_api::clean_returnvalue($returndescription, $result);
 270          $this->assertEquals($expectedquizzes, $result['quizzes']);
 271          $this->assertCount(0, $result['warnings']);
 272  
 273          // Unenrol user from second course and alter expected quizzes.
 274          $enrol->unenrol_user($instance2, $this->student->id);
 275          array_shift($expectedquizzes);
 276  
 277          // Call the external function without passing course id.
 278          $result = mod_quiz_external::get_quizzes_by_courses();
 279          $result = \external_api::clean_returnvalue($returndescription, $result);
 280          $this->assertEquals($expectedquizzes, $result['quizzes']);
 281  
 282          // Call for the second course we unenrolled the user from, expected warning.
 283          $result = mod_quiz_external::get_quizzes_by_courses(array($course2->id));
 284          $this->assertCount(1, $result['warnings']);
 285          $this->assertEquals('1', $result['warnings'][0]['warningcode']);
 286          $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
 287  
 288          // Now, try as a teacher for getting all the additional fields.
 289          self::setUser($this->teacher);
 290  
 291          foreach ($managerfields as $field) {
 292              $expectedquizzes[0][$field] = $quiz1->{$field};
 293          }
 294  
 295          $result = mod_quiz_external::get_quizzes_by_courses();
 296          $result = \external_api::clean_returnvalue($returndescription, $result);
 297          $this->assertEquals($expectedquizzes, $result['quizzes']);
 298  
 299          // Admin also should get all the information.
 300          self::setAdminUser();
 301  
 302          $result = mod_quiz_external::get_quizzes_by_courses(array($this->course->id));
 303          $result = \external_api::clean_returnvalue($returndescription, $result);
 304          $this->assertEquals($expectedquizzes, $result['quizzes']);
 305  
 306          // Now, prevent access.
 307          $enrol->enrol_user($instance2, $this->student->id);
 308  
 309          self::setUser($this->student);
 310  
 311          $quiz2->timeclose = time() - DAYSECS;
 312          $DB->update_record('quiz', $quiz2);
 313  
 314          $result = mod_quiz_external::get_quizzes_by_courses();
 315          $result = \external_api::clean_returnvalue($returndescription, $result);
 316          $this->assertCount(2, $result['quizzes']);
 317          // We only see a limited set of fields.
 318          $this->assertCount(4, $result['quizzes'][0]);
 319          $this->assertEquals($quiz2->id, $result['quizzes'][0]['id']);
 320          $this->assertEquals($quiz2->coursemodule, $result['quizzes'][0]['coursemodule']);
 321          $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']);
 322          $this->assertEquals($quiz2->name, $result['quizzes'][0]['name']);
 323          $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']);
 324  
 325          $this->assertFalse(isset($result['quizzes'][0]['timelimit']));
 326  
 327      }
 328  
 329      /**
 330       * Test test_view_quiz
 331       */
 332      public function test_view_quiz() {
 333          global $DB;
 334  
 335          // Test invalid instance id.
 336          try {
 337              mod_quiz_external::view_quiz(0);
 338              $this->fail('Exception expected due to invalid mod_quiz instance id.');
 339          } catch (\moodle_exception $e) {
 340              $this->assertEquals('invalidrecord', $e->errorcode);
 341          }
 342  
 343          // Test not-enrolled user.
 344          $usernotenrolled = self::getDataGenerator()->create_user();
 345          $this->setUser($usernotenrolled);
 346          try {
 347              mod_quiz_external::view_quiz($this->quiz->id);
 348              $this->fail('Exception expected due to not enrolled user.');
 349          } catch (\moodle_exception $e) {
 350              $this->assertEquals('requireloginerror', $e->errorcode);
 351          }
 352  
 353          // Test user with full capabilities.
 354          $this->setUser($this->student);
 355  
 356          // Trigger and capture the event.
 357          $sink = $this->redirectEvents();
 358  
 359          $result = mod_quiz_external::view_quiz($this->quiz->id);
 360          $result = \external_api::clean_returnvalue(mod_quiz_external::view_quiz_returns(), $result);
 361          $this->assertTrue($result['status']);
 362  
 363          $events = $sink->get_events();
 364          $this->assertCount(1, $events);
 365          $event = array_shift($events);
 366  
 367          // Checking that the event contains the expected values.
 368          $this->assertInstanceOf('\mod_quiz\event\course_module_viewed', $event);
 369          $this->assertEquals($this->context, $event->get_context());
 370          $moodlequiz = new \moodle_url('/mod/quiz/view.php', array('id' => $this->cm->id));
 371          $this->assertEquals($moodlequiz, $event->get_url());
 372          $this->assertEventContextNotUsed($event);
 373          $this->assertNotEmpty($event->get_name());
 374  
 375          // Test user with no capabilities.
 376          // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
 377          assign_capability('mod/quiz:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
 378          // Empty all the caches that may be affected  by this change.
 379          accesslib_clear_all_caches_for_unit_testing();
 380          \course_modinfo::clear_instance_cache();
 381  
 382          try {
 383              mod_quiz_external::view_quiz($this->quiz->id);
 384              $this->fail('Exception expected due to missing capability.');
 385          } catch (\moodle_exception $e) {
 386              $this->assertEquals('requireloginerror', $e->errorcode);
 387          }
 388  
 389      }
 390  
 391      /**
 392       * Test get_user_attempts
 393       */
 394      public function test_get_user_attempts() {
 395  
 396          // Create a quiz with one attempt finished.
 397          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true);
 398  
 399          $this->setUser($this->student);
 400          $result = mod_quiz_external::get_user_attempts($quiz->id);
 401          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 402  
 403          $this->assertCount(1, $result['attempts']);
 404          $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
 405          $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
 406          $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
 407          $this->assertEquals(1, $result['attempts'][0]['attempt']);
 408          $this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
 409          $this->assertEquals(1.0, $result['attempts'][0]['sumgrades']);
 410  
 411          // Test filters. Only finished.
 412          $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'finished', false);
 413          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 414  
 415          $this->assertCount(1, $result['attempts']);
 416          $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
 417  
 418          // Test filters. All attempts.
 419          $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
 420          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 421  
 422          $this->assertCount(1, $result['attempts']);
 423          $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
 424  
 425          // Test filters. Unfinished.
 426          $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
 427          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 428  
 429          $this->assertCount(0, $result['attempts']);
 430  
 431          // Start a new attempt, but not finish it.
 432          $timenow = time();
 433          $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
 434          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 435          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 436  
 437          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 438          quiz_attempt_save_started($quizobj, $quba, $attempt);
 439  
 440          // Test filters. All attempts.
 441          $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
 442          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 443  
 444          $this->assertCount(2, $result['attempts']);
 445  
 446          // Test filters. Unfinished.
 447          $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
 448          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 449  
 450          $this->assertCount(1, $result['attempts']);
 451  
 452          // Test manager can see user attempts.
 453          $this->setUser($this->teacher);
 454          $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id);
 455          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 456  
 457          $this->assertCount(1, $result['attempts']);
 458          $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
 459  
 460          $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'all');
 461          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 462  
 463          $this->assertCount(2, $result['attempts']);
 464          $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
 465  
 466          // Invalid parameters.
 467          try {
 468              mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'INVALID_PARAMETER');
 469              $this->fail('Exception expected due to missing capability.');
 470          } catch (\invalid_parameter_exception $e) {
 471              $this->assertEquals('invalidparameter', $e->errorcode);
 472          }
 473      }
 474  
 475      /**
 476       * Test get_user_attempts with marks hidden
 477       */
 478      public function test_get_user_attempts_with_marks_hidden() {
 479          // Create quiz with one attempt finished and hide the mark.
 480          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(
 481                  true, true, 'deferredfeedback', false,
 482                  ['marksduring' => 0, 'marksimmediately' => 0, 'marksopen' => 0, 'marksclosed' => 0]);
 483  
 484          // Student cannot see the grades.
 485          $this->setUser($this->student);
 486          $result = mod_quiz_external::get_user_attempts($quiz->id);
 487          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 488  
 489          $this->assertCount(1, $result['attempts']);
 490          $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
 491          $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
 492          $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
 493          $this->assertEquals(1, $result['attempts'][0]['attempt']);
 494          $this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
 495          $this->assertEquals(null, $result['attempts'][0]['sumgrades']);
 496  
 497          // Test manager can see user grades.
 498          $this->setUser($this->teacher);
 499          $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id);
 500          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 501  
 502          $this->assertCount(1, $result['attempts']);
 503          $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
 504          $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
 505          $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
 506          $this->assertEquals(1, $result['attempts'][0]['attempt']);
 507          $this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
 508          $this->assertEquals(1.0, $result['attempts'][0]['sumgrades']);
 509      }
 510  
 511      /**
 512       * Test get_user_best_grade
 513       */
 514      public function test_get_user_best_grade() {
 515          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 516          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 517          $questioncat = $questiongenerator->create_question_category();
 518  
 519          // Create a new quiz.
 520          $quizapi1 = $quizgenerator->create_instance([
 521                  'name' => 'Test Quiz API 1',
 522                  'course' => $this->course->id,
 523                  'sumgrades' => 1
 524          ]);
 525          $quizapi2 = $quizgenerator->create_instance([
 526                  'name' => 'Test Quiz API 2',
 527                  'course' => $this->course->id,
 528                  'sumgrades' => 1,
 529                  'marksduring' => 0,
 530                  'marksimmediately' => 0,
 531                  'marksopen' => 0,
 532                  'marksclosed' => 0
 533          ]);
 534  
 535          // Create a question.
 536          $question = $questiongenerator->create_question('numerical', null, ['category' => $questioncat->id]);
 537  
 538          // Add question to the quizzes.
 539          quiz_add_quiz_question($question->id, $quizapi1);
 540          quiz_add_quiz_question($question->id, $quizapi2);
 541  
 542          // Create quiz object.
 543          $quizapiobj1 = quiz::create($quizapi1->id, $this->student->id);
 544          $quizapiobj2 = quiz::create($quizapi2->id, $this->student->id);
 545  
 546          // Set grade to pass.
 547          $item = \grade_item::fetch([
 548                  'courseid' => $this->course->id,
 549                  'itemtype' => 'mod',
 550                  'itemmodule' => 'quiz',
 551                  'iteminstance' => $quizapi1->id,
 552                  'outcomeid' => null
 553          ]);
 554          $item->gradepass = 80;
 555          $item->update();
 556  
 557          $item = \grade_item::fetch([
 558                  'courseid' => $this->course->id,
 559                  'itemtype' => 'mod',
 560                  'itemmodule' => 'quiz',
 561                  'iteminstance' => $quizapi2->id,
 562                  'outcomeid' => null
 563          ]);
 564          $item->gradepass = 80;
 565          $item->update();
 566  
 567          // Start the passing attempt.
 568          $quba1 = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizapiobj1->get_context());
 569          $quba1->set_preferred_behaviour($quizapiobj1->get_quiz()->preferredbehaviour);
 570  
 571          $quba2 = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizapiobj2->get_context());
 572          $quba2->set_preferred_behaviour($quizapiobj2->get_quiz()->preferredbehaviour);
 573  
 574          // Start the testing for quizapi1 that allow the student to view the grade.
 575  
 576          $this->setUser($this->student);
 577          $result = mod_quiz_external::get_user_best_grade($quizapi1->id);
 578          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
 579  
 580          // No grades yet.
 581          $this->assertFalse($result['hasgrade']);
 582          $this->assertTrue(!isset($result['grade']));
 583  
 584          // Start the attempt.
 585          $timenow = time();
 586          $attempt = quiz_create_attempt($quizapiobj1, 1, false, $timenow, false, $this->student->id);
 587          quiz_start_new_attempt($quizapiobj1, $quba1, $attempt, 1, $timenow);
 588          quiz_attempt_save_started($quizapiobj1, $quba1, $attempt);
 589  
 590          // Process some responses from the student.
 591          $attemptobj = quiz_attempt::create($attempt->id);
 592          $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]);
 593  
 594          // Finish the attempt.
 595          $attemptobj->process_finish($timenow, false);
 596  
 597          $result = mod_quiz_external::get_user_best_grade($quizapi1->id);
 598          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
 599  
 600          // Now I have grades.
 601          $this->assertTrue($result['hasgrade']);
 602          $this->assertEquals(100.0, $result['grade']);
 603          $this->assertEquals(80, $result['gradetopass']);
 604  
 605          // We should not see other users grades.
 606          $anotherstudent = self::getDataGenerator()->create_user();
 607          $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
 608  
 609          try {
 610              mod_quiz_external::get_user_best_grade($quizapi1->id, $anotherstudent->id);
 611              $this->fail('Exception expected due to missing capability.');
 612          } catch (\required_capability_exception $e) {
 613              $this->assertEquals('nopermissions', $e->errorcode);
 614          }
 615  
 616          // Teacher must be able to see student grades.
 617          $this->setUser($this->teacher);
 618  
 619          $result = mod_quiz_external::get_user_best_grade($quizapi1->id, $this->student->id);
 620          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
 621  
 622          $this->assertTrue($result['hasgrade']);
 623          $this->assertEquals(100.0, $result['grade']);
 624          $this->assertEquals(80, $result['gradetopass']);
 625  
 626          // Invalid user.
 627          try {
 628              mod_quiz_external::get_user_best_grade($this->quiz->id, -1);
 629              $this->fail('Exception expected due to missing capability.');
 630          } catch (\dml_missing_record_exception $e) {
 631              $this->assertEquals('invaliduser', $e->errorcode);
 632          }
 633  
 634          // End the testing for quizapi1 that allow the student to view the grade.
 635  
 636          // Start the testing for quizapi2 that do not allow the student to view the grade.
 637  
 638          $this->setUser($this->student);
 639          $result = mod_quiz_external::get_user_best_grade($quizapi2->id);
 640          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
 641  
 642          // No grades yet.
 643          $this->assertFalse($result['hasgrade']);
 644          $this->assertTrue(!isset($result['grade']));
 645  
 646          // Start the attempt.
 647          $timenow = time();
 648          $attempt = quiz_create_attempt($quizapiobj2, 1, false, $timenow, false, $this->student->id);
 649          quiz_start_new_attempt($quizapiobj2, $quba2, $attempt, 1, $timenow);
 650          quiz_attempt_save_started($quizapiobj2, $quba2, $attempt);
 651  
 652          // Process some responses from the student.
 653          $attemptobj = quiz_attempt::create($attempt->id);
 654          $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]);
 655  
 656          // Finish the attempt.
 657          $attemptobj->process_finish($timenow, false);
 658  
 659          $result = mod_quiz_external::get_user_best_grade($quizapi2->id);
 660          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
 661  
 662          // Now I have grades but I will not be allowed to see it.
 663          $this->assertFalse($result['hasgrade']);
 664          $this->assertTrue(!isset($result['grade']));
 665  
 666          // Teacher must be able to see student grades.
 667          $this->setUser($this->teacher);
 668  
 669          $result = mod_quiz_external::get_user_best_grade($quizapi2->id, $this->student->id);
 670          $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
 671  
 672          $this->assertTrue($result['hasgrade']);
 673          $this->assertEquals(100.0, $result['grade']);
 674  
 675          // End the testing for quizapi2 that do not allow the student to view the grade.
 676  
 677      }
 678      /**
 679       * Test get_combined_review_options.
 680       * This is a basic test, this is already tested in mod_quiz_display_options_testcase.
 681       */
 682      public function test_get_combined_review_options() {
 683          global $DB;
 684  
 685          // Create a new quiz with attempts.
 686          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 687          $data = array('course' => $this->course->id,
 688                        'sumgrades' => 1);
 689          $quiz = $quizgenerator->create_instance($data);
 690  
 691          // Create a couple of questions.
 692          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 693  
 694          $cat = $questiongenerator->create_question_category();
 695          $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
 696          quiz_add_quiz_question($question->id, $quiz);
 697  
 698          $quizobj = quiz::create($quiz->id, $this->student->id);
 699  
 700          // Set grade to pass.
 701          $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
 702                                          'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
 703          $item->gradepass = 80;
 704          $item->update();
 705  
 706          // Start the passing attempt.
 707          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 708          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 709  
 710          $timenow = time();
 711          $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
 712          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 713          quiz_attempt_save_started($quizobj, $quba, $attempt);
 714  
 715          $this->setUser($this->student);
 716  
 717          $result = mod_quiz_external::get_combined_review_options($quiz->id);
 718          $result = \external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
 719  
 720          // Expected values.
 721          $expected = array(
 722              "someoptions" => array(
 723                  array("name" => "feedback", "value" => 1),
 724                  array("name" => "generalfeedback", "value" => 1),
 725                  array("name" => "rightanswer", "value" => 1),
 726                  array("name" => "overallfeedback", "value" => 0),
 727                  array("name" => "marks", "value" => 2),
 728              ),
 729              "alloptions" => array(
 730                  array("name" => "feedback", "value" => 1),
 731                  array("name" => "generalfeedback", "value" => 1),
 732                  array("name" => "rightanswer", "value" => 1),
 733                  array("name" => "overallfeedback", "value" => 0),
 734                  array("name" => "marks", "value" => 2),
 735              ),
 736              "warnings" => [],
 737          );
 738  
 739          $this->assertEquals($expected, $result);
 740  
 741          // Now, finish the attempt.
 742          $attemptobj = quiz_attempt::create($attempt->id);
 743          $attemptobj->process_finish($timenow, false);
 744  
 745          $expected = array(
 746              "someoptions" => array(
 747                  array("name" => "feedback", "value" => 1),
 748                  array("name" => "generalfeedback", "value" => 1),
 749                  array("name" => "rightanswer", "value" => 1),
 750                  array("name" => "overallfeedback", "value" => 1),
 751                  array("name" => "marks", "value" => 2),
 752              ),
 753              "alloptions" => array(
 754                  array("name" => "feedback", "value" => 1),
 755                  array("name" => "generalfeedback", "value" => 1),
 756                  array("name" => "rightanswer", "value" => 1),
 757                  array("name" => "overallfeedback", "value" => 1),
 758                  array("name" => "marks", "value" => 2),
 759              ),
 760              "warnings" => [],
 761          );
 762  
 763          // We should see now the overall feedback.
 764          $result = mod_quiz_external::get_combined_review_options($quiz->id);
 765          $result = \external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
 766          $this->assertEquals($expected, $result);
 767  
 768          // Start a new attempt, but not finish it.
 769          $timenow = time();
 770          $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
 771          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 772          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 773          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 774          quiz_attempt_save_started($quizobj, $quba, $attempt);
 775  
 776          $expected = array(
 777              "someoptions" => array(
 778                  array("name" => "feedback", "value" => 1),
 779                  array("name" => "generalfeedback", "value" => 1),
 780                  array("name" => "rightanswer", "value" => 1),
 781                  array("name" => "overallfeedback", "value" => 1),
 782                  array("name" => "marks", "value" => 2),
 783              ),
 784              "alloptions" => array(
 785                  array("name" => "feedback", "value" => 1),
 786                  array("name" => "generalfeedback", "value" => 1),
 787                  array("name" => "rightanswer", "value" => 1),
 788                  array("name" => "overallfeedback", "value" => 0),
 789                  array("name" => "marks", "value" => 2),
 790              ),
 791              "warnings" => [],
 792          );
 793  
 794          $result = mod_quiz_external::get_combined_review_options($quiz->id);
 795          $result = \external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
 796          $this->assertEquals($expected, $result);
 797  
 798          // Teacher, for see student options.
 799          $this->setUser($this->teacher);
 800  
 801          $result = mod_quiz_external::get_combined_review_options($quiz->id, $this->student->id);
 802          $result = \external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
 803  
 804          $this->assertEquals($expected, $result);
 805  
 806          // Invalid user.
 807          try {
 808              mod_quiz_external::get_combined_review_options($quiz->id, -1);
 809              $this->fail('Exception expected due to missing capability.');
 810          } catch (\dml_missing_record_exception $e) {
 811              $this->assertEquals('invaliduser', $e->errorcode);
 812          }
 813      }
 814  
 815      /**
 816       * Test start_attempt
 817       */
 818      public function test_start_attempt() {
 819          global $DB;
 820  
 821          // Create a new quiz with questions.
 822          list($quiz, $context, $quizobj) = $this->create_quiz_with_questions();
 823  
 824          $this->setUser($this->student);
 825  
 826          // Try to open attempt in closed quiz.
 827          $quiz->timeopen = time() - WEEKSECS;
 828          $quiz->timeclose = time() - DAYSECS;
 829          $DB->update_record('quiz', $quiz);
 830          $result = mod_quiz_external::start_attempt($quiz->id);
 831          $result = \external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
 832  
 833          $this->assertEquals([], $result['attempt']);
 834          $this->assertCount(1, $result['warnings']);
 835  
 836          // Now with a password.
 837          $quiz->timeopen = 0;
 838          $quiz->timeclose = 0;
 839          $quiz->password = 'abc';
 840          $DB->update_record('quiz', $quiz);
 841  
 842          try {
 843              mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'bad')));
 844              $this->fail('Exception expected due to invalid passwod.');
 845          } catch (\moodle_exception $e) {
 846              $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode);
 847          }
 848  
 849          // Now, try everything correct.
 850          $result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc')));
 851          $result = \external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
 852  
 853          $this->assertEquals(1, $result['attempt']['attempt']);
 854          $this->assertEquals($this->student->id, $result['attempt']['userid']);
 855          $this->assertEquals($quiz->id, $result['attempt']['quiz']);
 856          $this->assertCount(0, $result['warnings']);
 857          $attemptid = $result['attempt']['id'];
 858  
 859          // We are good, try to start a new attempt now.
 860  
 861          try {
 862              mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc')));
 863              $this->fail('Exception expected due to attempt not finished.');
 864          } catch (\moodle_quiz_exception $e) {
 865              $this->assertEquals('attemptstillinprogress', $e->errorcode);
 866          }
 867  
 868          // Finish the started attempt.
 869  
 870          // Process some responses from the student.
 871          $timenow = time();
 872          $attemptobj = quiz_attempt::create($attemptid);
 873          $tosubmit = array(1 => array('answer' => '3.14'));
 874          $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
 875  
 876          // Finish the attempt.
 877          $attemptobj = quiz_attempt::create($attemptid);
 878          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 879          $attemptobj->process_finish($timenow, false);
 880  
 881          // We should be able to start a new attempt.
 882          $result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc')));
 883          $result = \external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
 884  
 885          $this->assertEquals(2, $result['attempt']['attempt']);
 886          $this->assertEquals($this->student->id, $result['attempt']['userid']);
 887          $this->assertEquals($quiz->id, $result['attempt']['quiz']);
 888          $this->assertCount(0, $result['warnings']);
 889  
 890          // Test user with no capabilities.
 891          // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
 892          assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id);
 893          // Empty all the caches that may be affected  by this change.
 894          accesslib_clear_all_caches_for_unit_testing();
 895          \course_modinfo::clear_instance_cache();
 896  
 897          try {
 898              mod_quiz_external::start_attempt($quiz->id);
 899              $this->fail('Exception expected due to missing capability.');
 900          } catch (\required_capability_exception $e) {
 901              $this->assertEquals('nopermissions', $e->errorcode);
 902          }
 903  
 904      }
 905  
 906      /**
 907       * Test validate_attempt
 908       */
 909      public function test_validate_attempt() {
 910          global $DB;
 911  
 912          // Create a new quiz with one attempt started.
 913          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
 914  
 915          $this->setUser($this->student);
 916  
 917          // Invalid attempt.
 918          try {
 919              $params = array('attemptid' => -1, 'page' => 0);
 920              testable_mod_quiz_external::validate_attempt($params);
 921              $this->fail('Exception expected due to invalid attempt id.');
 922          } catch (\dml_missing_record_exception $e) {
 923              $this->assertEquals('invalidrecord', $e->errorcode);
 924          }
 925  
 926          // Test OK case.
 927          $params = array('attemptid' => $attempt->id, 'page' => 0);
 928          $result = testable_mod_quiz_external::validate_attempt($params);
 929          $this->assertEquals($attempt->id, $result[0]->get_attempt()->id);
 930          $this->assertEquals([], $result[1]);
 931  
 932          // Test with preflight data.
 933          $quiz->password = 'abc';
 934          $DB->update_record('quiz', $quiz);
 935  
 936          try {
 937              $params = array('attemptid' => $attempt->id, 'page' => 0,
 938                              'preflightdata' => array(array("name" => "quizpassword", "value" => 'bad')));
 939              testable_mod_quiz_external::validate_attempt($params);
 940              $this->fail('Exception expected due to invalid passwod.');
 941          } catch (\moodle_exception $e) {
 942              $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode);
 943          }
 944  
 945          // Now, try everything correct.
 946          $params['preflightdata'][0]['value'] = 'abc';
 947          $result = testable_mod_quiz_external::validate_attempt($params);
 948          $this->assertEquals($attempt->id, $result[0]->get_attempt()->id);
 949          $this->assertEquals([], $result[1]);
 950  
 951          // Page out of range.
 952          $DB->update_record('quiz', $quiz);
 953          $params['page'] = 4;
 954          try {
 955              testable_mod_quiz_external::validate_attempt($params);
 956              $this->fail('Exception expected due to page out of range.');
 957          } catch (\moodle_quiz_exception $e) {
 958              $this->assertEquals('Invalid page number', $e->errorcode);
 959          }
 960  
 961          $params['page'] = 0;
 962          // Try to open attempt in closed quiz.
 963          $quiz->timeopen = time() - WEEKSECS;
 964          $quiz->timeclose = time() - DAYSECS;
 965          $DB->update_record('quiz', $quiz);
 966  
 967          // This should work, ommit access rules.
 968          testable_mod_quiz_external::validate_attempt($params, false);
 969  
 970          // Get a generic error because prior to checking the dates the attempt is closed.
 971          try {
 972              testable_mod_quiz_external::validate_attempt($params);
 973              $this->fail('Exception expected due to passed dates.');
 974          } catch (\moodle_quiz_exception $e) {
 975              $this->assertEquals('attempterror', $e->errorcode);
 976          }
 977  
 978          // Finish the attempt.
 979          $attemptobj = quiz_attempt::create($attempt->id);
 980          $attemptobj->process_finish(time(), false);
 981  
 982          try {
 983              testable_mod_quiz_external::validate_attempt($params, false);
 984              $this->fail('Exception expected due to attempt finished.');
 985          } catch (\moodle_quiz_exception $e) {
 986              $this->assertEquals('attemptalreadyclosed', $e->errorcode);
 987          }
 988  
 989          // Test user with no capabilities.
 990          // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
 991          assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id);
 992          // Empty all the caches that may be affected  by this change.
 993          accesslib_clear_all_caches_for_unit_testing();
 994          \course_modinfo::clear_instance_cache();
 995  
 996          try {
 997              testable_mod_quiz_external::validate_attempt($params);
 998              $this->fail('Exception expected due to missing permissions.');
 999          } catch (\required_capability_exception $e) {
1000              $this->assertEquals('nopermissions', $e->errorcode);
1001          }
1002  
1003          // Now try with a different user.
1004          $this->setUser($this->teacher);
1005  
1006          $params['page'] = 0;
1007          try {
1008              testable_mod_quiz_external::validate_attempt($params);
1009              $this->fail('Exception expected due to not your attempt.');
1010          } catch (\moodle_quiz_exception $e) {
1011              $this->assertEquals('notyourattempt', $e->errorcode);
1012          }
1013      }
1014  
1015      /**
1016       * Test get_attempt_data
1017       */
1018      public function test_get_attempt_data() {
1019          global $DB;
1020  
1021          $timenow = time();
1022          // Create a new quiz with one attempt started.
1023          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
1024  
1025          // Set correctness mask so questions state can be fetched only after finishing the attempt.
1026          $DB->set_field('quiz', 'reviewcorrectness', mod_quiz_display_options::IMMEDIATELY_AFTER, array('id' => $quiz->id));
1027  
1028          $quizobj = $attemptobj->get_quizobj();
1029          $quizobj->preload_questions();
1030          $quizobj->load_questions();
1031          $questions = $quizobj->get_questions();
1032  
1033          $this->setUser($this->student);
1034  
1035          // We receive one question per page.
1036          $result = mod_quiz_external::get_attempt_data($attempt->id, 0);
1037          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
1038  
1039          $this->assertEquals($attempt, (object) $result['attempt']);
1040          $this->assertEquals(1, $result['nextpage']);
1041          $this->assertCount(0, $result['messages']);
1042          $this->assertCount(1, $result['questions']);
1043          $this->assertEquals(1, $result['questions'][0]['slot']);
1044          $this->assertEquals(1, $result['questions'][0]['number']);
1045          $this->assertEquals('numerical', $result['questions'][0]['type']);
1046          $this->assertArrayNotHasKey('state', $result['questions'][0]);  // We don't receive the state yet.
1047          $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
1048          $this->assertFalse($result['questions'][0]['flagged']);
1049          $this->assertEquals(0, $result['questions'][0]['page']);
1050          $this->assertEmpty($result['questions'][0]['mark']);
1051          $this->assertEquals(1, $result['questions'][0]['maxmark']);
1052          $this->assertEquals(1, $result['questions'][0]['sequencecheck']);
1053          $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']);
1054          $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
1055  
1056          // Now try the last page.
1057          $result = mod_quiz_external::get_attempt_data($attempt->id, 1);
1058          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
1059  
1060          $this->assertEquals($attempt, (object) $result['attempt']);
1061          $this->assertEquals(-1, $result['nextpage']);
1062          $this->assertCount(0, $result['messages']);
1063          $this->assertCount(1, $result['questions']);
1064          $this->assertEquals(2, $result['questions'][0]['slot']);
1065          $this->assertEquals(2, $result['questions'][0]['number']);
1066          $this->assertEquals('numerical', $result['questions'][0]['type']);
1067          $this->assertArrayNotHasKey('state', $result['questions'][0]);  // We don't receive the state yet.
1068          $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
1069          $this->assertFalse($result['questions'][0]['flagged']);
1070          $this->assertEquals(1, $result['questions'][0]['page']);
1071          $this->assertEquals(1, $result['questions'][0]['sequencecheck']);
1072          $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']);
1073          $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
1074  
1075          // Finish previous attempt.
1076          $attemptobj->process_finish(time(), false);
1077  
1078          // Now we should receive the question state.
1079          $result = mod_quiz_external::get_attempt_review($attempt->id, 1);
1080          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
1081          $this->assertEquals('gaveup', $result['questions'][0]['state']);
1082  
1083          // Change setting and expect two pages.
1084          $quiz->questionsperpage = 4;
1085          $DB->update_record('quiz', $quiz);
1086          quiz_repaginate_questions($quiz->id, $quiz->questionsperpage);
1087  
1088          // Start with new attempt with the new layout.
1089          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1090          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1091  
1092          $timenow = time();
1093          $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
1094          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
1095          quiz_attempt_save_started($quizobj, $quba, $attempt);
1096  
1097          // We receive two questions per page.
1098          $result = mod_quiz_external::get_attempt_data($attempt->id, 0);
1099          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
1100          $this->assertCount(2, $result['questions']);
1101          $this->assertEquals(-1, $result['nextpage']);
1102  
1103          // Check questions looks good.
1104          $found = 0;
1105          foreach ($questions as $question) {
1106              foreach ($result['questions'] as $rquestion) {
1107                  if ($rquestion['slot'] == $question->slot) {
1108                      $this->assertTrue(strpos($rquestion['html'], "qid=$question->id") !== false);
1109                      $found++;
1110                  }
1111              }
1112          }
1113          $this->assertEquals(2, $found);
1114  
1115      }
1116  
1117      /**
1118       * Test get_attempt_data with blocked questions.
1119       * @since 3.2
1120       */
1121      public function test_get_attempt_data_with_blocked_questions() {
1122          global $DB;
1123  
1124          // Create a new quiz with one attempt started and using immediatefeedback.
1125          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(
1126                  true, false, 'immediatefeedback');
1127  
1128          $quizobj = $attemptobj->get_quizobj();
1129  
1130          // Make second question blocked by the first one.
1131          $structure = $quizobj->get_structure();
1132          $slots = $structure->get_slots();
1133          $structure->update_question_dependency(end($slots)->id, true);
1134  
1135          $quizobj->preload_questions();
1136          $quizobj->load_questions();
1137          $questions = $quizobj->get_questions();
1138  
1139          $this->setUser($this->student);
1140  
1141          // We receive one question per page.
1142          $result = mod_quiz_external::get_attempt_data($attempt->id, 0);
1143          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
1144  
1145          $this->assertEquals($attempt, (object) $result['attempt']);
1146          $this->assertCount(1, $result['questions']);
1147          $this->assertEquals(1, $result['questions'][0]['slot']);
1148          $this->assertEquals(1, $result['questions'][0]['number']);
1149          $this->assertEquals(false, $result['questions'][0]['blockedbyprevious']);
1150  
1151          // Now try the last page.
1152          $result = mod_quiz_external::get_attempt_data($attempt->id, 1);
1153          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
1154  
1155          $this->assertEquals($attempt, (object) $result['attempt']);
1156          $this->assertCount(1, $result['questions']);
1157          $this->assertEquals(2, $result['questions'][0]['slot']);
1158          $this->assertEquals(2, $result['questions'][0]['number']);
1159          $this->assertEquals(true, $result['questions'][0]['blockedbyprevious']);
1160      }
1161  
1162      /**
1163       * Test get_attempt_summary
1164       */
1165      public function test_get_attempt_summary() {
1166  
1167          $timenow = time();
1168          // Create a new quiz with one attempt started.
1169          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
1170  
1171          $this->setUser($this->student);
1172          $result = mod_quiz_external::get_attempt_summary($attempt->id);
1173          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1174  
1175          // Check the state, flagged and mark data is correct.
1176          $this->assertEquals('todo', $result['questions'][0]['state']);
1177          $this->assertEquals('todo', $result['questions'][1]['state']);
1178          $this->assertEquals(1, $result['questions'][0]['number']);
1179          $this->assertEquals(2, $result['questions'][1]['number']);
1180          $this->assertFalse($result['questions'][0]['flagged']);
1181          $this->assertFalse($result['questions'][1]['flagged']);
1182          $this->assertEmpty($result['questions'][0]['mark']);
1183          $this->assertEmpty($result['questions'][1]['mark']);
1184          $this->assertEquals(1, $result['questions'][0]['sequencecheck']);
1185          $this->assertEquals(1, $result['questions'][1]['sequencecheck']);
1186          $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']);
1187          $this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']);
1188          $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
1189          $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']);
1190  
1191          // Check question options.
1192          $this->assertNotEmpty(5, $result['questions'][0]['settings']);
1193          // Check at least some settings returned.
1194          $this->assertCount(4, (array) json_decode($result['questions'][0]['settings']));
1195  
1196          // Submit a response for the first question.
1197          $tosubmit = array(1 => array('answer' => '3.14'));
1198          $attemptobj->process_submitted_actions(time(), false, $tosubmit);
1199          $result = mod_quiz_external::get_attempt_summary($attempt->id);
1200          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1201  
1202          // Check it's marked as completed only the first one.
1203          $this->assertEquals('complete', $result['questions'][0]['state']);
1204          $this->assertEquals('todo', $result['questions'][1]['state']);
1205          $this->assertEquals(1, $result['questions'][0]['number']);
1206          $this->assertEquals(2, $result['questions'][1]['number']);
1207          $this->assertFalse($result['questions'][0]['flagged']);
1208          $this->assertFalse($result['questions'][1]['flagged']);
1209          $this->assertEmpty($result['questions'][0]['mark']);
1210          $this->assertEmpty($result['questions'][1]['mark']);
1211          $this->assertEquals(2, $result['questions'][0]['sequencecheck']);
1212          $this->assertEquals(1, $result['questions'][1]['sequencecheck']);
1213          $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']);
1214          $this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']);
1215          $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
1216          $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']);
1217  
1218      }
1219  
1220      /**
1221       * Test save_attempt
1222       */
1223      public function test_save_attempt() {
1224  
1225          $timenow = time();
1226          // Create a new quiz with one attempt started.
1227          list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true);
1228  
1229          // Response for slot 1.
1230          $prefix = $quba->get_field_prefix(1);
1231          $data = array(
1232              array('name' => 'slots', 'value' => 1),
1233              array('name' => $prefix . ':sequencecheck',
1234                      'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
1235              array('name' => $prefix . 'answer', 'value' => 1),
1236          );
1237  
1238          $this->setUser($this->student);
1239  
1240          $result = mod_quiz_external::save_attempt($attempt->id, $data);
1241          $result = \external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result);
1242          $this->assertTrue($result['status']);
1243  
1244          // Now, get the summary.
1245          $result = mod_quiz_external::get_attempt_summary($attempt->id);
1246          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1247  
1248          // Check it's marked as completed only the first one.
1249          $this->assertEquals('complete', $result['questions'][0]['state']);
1250          $this->assertEquals('todo', $result['questions'][1]['state']);
1251          $this->assertEquals(1, $result['questions'][0]['number']);
1252          $this->assertEquals(2, $result['questions'][1]['number']);
1253          $this->assertFalse($result['questions'][0]['flagged']);
1254          $this->assertFalse($result['questions'][1]['flagged']);
1255          $this->assertEmpty($result['questions'][0]['mark']);
1256          $this->assertEmpty($result['questions'][1]['mark']);
1257          $this->assertEquals(1, $result['questions'][0]['sequencecheck']);
1258          $this->assertEquals(1, $result['questions'][1]['sequencecheck']);
1259          $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']);
1260          $this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']);
1261          $this->assertEquals(true, $result['questions'][0]['hasautosavedstep']);
1262          $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']);
1263  
1264          // Now, second slot.
1265          $prefix = $quba->get_field_prefix(2);
1266          $data = array(
1267              array('name' => 'slots', 'value' => 2),
1268              array('name' => $prefix . ':sequencecheck',
1269                      'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
1270              array('name' => $prefix . 'answer', 'value' => 1),
1271          );
1272  
1273          $result = mod_quiz_external::save_attempt($attempt->id, $data);
1274          $result = \external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result);
1275          $this->assertTrue($result['status']);
1276  
1277          // Now, get the summary.
1278          $result = mod_quiz_external::get_attempt_summary($attempt->id);
1279          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1280  
1281          // Check it's marked as completed only the first one.
1282          $this->assertEquals('complete', $result['questions'][0]['state']);
1283          $this->assertEquals(1, $result['questions'][0]['sequencecheck']);
1284          $this->assertEquals('complete', $result['questions'][1]['state']);
1285          $this->assertEquals(1, $result['questions'][1]['sequencecheck']);
1286  
1287      }
1288  
1289      /**
1290       * Test process_attempt
1291       */
1292      public function test_process_attempt() {
1293          global $DB;
1294  
1295          $timenow = time();
1296          // Create a new quiz with three questions and one attempt started.
1297          list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false,
1298              'deferredfeedback', true);
1299  
1300          // Response for slot 1.
1301          $prefix = $quba->get_field_prefix(1);
1302          $data = array(
1303              array('name' => 'slots', 'value' => 1),
1304              array('name' => $prefix . ':sequencecheck',
1305                      'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
1306              array('name' => $prefix . 'answer', 'value' => 1),
1307          );
1308  
1309          $this->setUser($this->student);
1310  
1311          $result = mod_quiz_external::process_attempt($attempt->id, $data);
1312          $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1313          $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']);
1314  
1315          $result = mod_quiz_external::get_attempt_data($attempt->id, 2);
1316  
1317          // Now, get the summary.
1318          $result = mod_quiz_external::get_attempt_summary($attempt->id);
1319          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1320          $this->assertDebuggingCalled(); // Expect $PAGE->set_url debugging.
1321  
1322          // Check it's marked as completed only the first one.
1323          $this->assertEquals('complete', $result['questions'][0]['state']);
1324          $this->assertEquals('todo', $result['questions'][1]['state']);
1325          $this->assertEquals(1, $result['questions'][0]['number']);
1326          $this->assertEquals(2, $result['questions'][1]['number']);
1327          $this->assertFalse($result['questions'][0]['flagged']);
1328          $this->assertFalse($result['questions'][1]['flagged']);
1329          $this->assertEmpty($result['questions'][0]['mark']);
1330          $this->assertEmpty($result['questions'][1]['mark']);
1331          $this->assertEquals(2, $result['questions'][0]['sequencecheck']);
1332          $this->assertEquals(2, $result['questions'][0]['sequencecheck']);
1333          $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']);
1334          $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']);
1335          $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
1336          $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
1337  
1338          // Now, second slot.
1339          $prefix = $quba->get_field_prefix(2);
1340          $data = array(
1341              array('name' => 'slots', 'value' => 2),
1342              array('name' => $prefix . ':sequencecheck',
1343                      'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
1344              array('name' => $prefix . 'answer', 'value' => 1),
1345              array('name' => $prefix . ':flagged', 'value' => 1),
1346          );
1347  
1348          $result = mod_quiz_external::process_attempt($attempt->id, $data);
1349          $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1350          $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']);
1351  
1352          // Now, get the summary.
1353          $result = mod_quiz_external::get_attempt_summary($attempt->id);
1354          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1355  
1356          // Check it's marked as completed the two first questions.
1357          $this->assertEquals('complete', $result['questions'][0]['state']);
1358          $this->assertEquals('complete', $result['questions'][1]['state']);
1359          $this->assertFalse($result['questions'][0]['flagged']);
1360          $this->assertTrue($result['questions'][1]['flagged']);
1361  
1362          // Add files in the attachment response.
1363          $draftitemid = file_get_unused_draft_itemid();
1364          $filerecordinline = array(
1365              'contextid' => \context_user::instance($this->student->id)->id,
1366              'component' => 'user',
1367              'filearea'  => 'draft',
1368              'itemid'    => $draftitemid,
1369              'filepath'  => '/',
1370              'filename'  => 'faketxt.txt',
1371          );
1372          $fs = get_file_storage();
1373          $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.');
1374  
1375          // Last slot.
1376          $prefix = $quba->get_field_prefix(3);
1377          $data = array(
1378              array('name' => 'slots', 'value' => 3),
1379              array('name' => $prefix . ':sequencecheck',
1380                      'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
1381              array('name' => $prefix . 'answer', 'value' => 'Some test'),
1382              array('name' => $prefix . 'answerformat', 'value' => FORMAT_HTML),
1383              array('name' => $prefix . 'attachments', 'value' => $draftitemid),
1384          );
1385  
1386          $result = mod_quiz_external::process_attempt($attempt->id, $data);
1387          $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1388          $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']);
1389  
1390          // Now, get the summary.
1391          $result = mod_quiz_external::get_attempt_summary($attempt->id);
1392          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1393  
1394          $this->assertEquals('complete', $result['questions'][0]['state']);
1395          $this->assertEquals('complete', $result['questions'][1]['state']);
1396          $this->assertEquals('complete', $result['questions'][2]['state']);
1397          $this->assertFalse($result['questions'][0]['flagged']);
1398          $this->assertTrue($result['questions'][1]['flagged']);
1399          $this->assertFalse($result['questions'][2]['flagged']);
1400  
1401          // Check submitted files are there.
1402          $this->assertCount(1, $result['questions'][2]['responsefileareas']);
1403          $this->assertEquals('attachments', $result['questions'][2]['responsefileareas'][0]['area']);
1404          $this->assertCount(1, $result['questions'][2]['responsefileareas'][0]['files']);
1405          $this->assertEquals($filerecordinline['filename'], $result['questions'][2]['responsefileareas'][0]['files'][0]['filename']);
1406  
1407          // Finish the attempt.
1408          $sink = $this->redirectMessages();
1409          $result = mod_quiz_external::process_attempt($attempt->id, array(), true);
1410          $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1411          $this->assertEquals(quiz_attempt::FINISHED, $result['state']);
1412          $messages = $sink->get_messages();
1413          $message = reset($messages);
1414          $sink->close();
1415          // Test customdata.
1416          if (!empty($message->customdata)) {
1417              $customdata = json_decode($message->customdata);
1418              $this->assertEquals($quizobj->get_quizid(), $customdata->instance);
1419              $this->assertEquals($quizobj->get_cmid(), $customdata->cmid);
1420              $this->assertEquals($attempt->id, $customdata->attemptid);
1421              $this->assertObjectHasAttribute('notificationiconurl', $customdata);
1422          }
1423  
1424          // Start new attempt.
1425          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1426          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1427  
1428          $timenow = time();
1429          $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
1430          quiz_start_new_attempt($quizobj, $quba, $attempt, 2, $timenow);
1431          quiz_attempt_save_started($quizobj, $quba, $attempt);
1432  
1433          // Force grace period, attempt going to overdue.
1434          $quiz->timeclose = $timenow - 10;
1435          $quiz->graceperiod = 60;
1436          $quiz->overduehandling = 'graceperiod';
1437          $DB->update_record('quiz', $quiz);
1438  
1439          $result = mod_quiz_external::process_attempt($attempt->id, array());
1440          $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1441          $this->assertEquals(quiz_attempt::OVERDUE, $result['state']);
1442  
1443          // Force grace period for time limit.
1444          $quiz->timeclose = 0;
1445          $quiz->timelimit = 1;
1446          $quiz->graceperiod = 60;
1447          $quiz->overduehandling = 'graceperiod';
1448          $DB->update_record('quiz', $quiz);
1449  
1450          $timenow = time();
1451          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1452          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1453          $attempt = quiz_create_attempt($quizobj, 3, 2, $timenow - 10, false, $this->student->id);
1454          quiz_start_new_attempt($quizobj, $quba, $attempt, 2, $timenow - 10);
1455          quiz_attempt_save_started($quizobj, $quba, $attempt);
1456  
1457          $result = mod_quiz_external::process_attempt($attempt->id, array());
1458          $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1459          $this->assertEquals(quiz_attempt::OVERDUE, $result['state']);
1460  
1461          // New attempt.
1462          $timenow = time();
1463          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1464          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1465          $attempt = quiz_create_attempt($quizobj, 4, 3, $timenow, false, $this->student->id);
1466          quiz_start_new_attempt($quizobj, $quba, $attempt, 3, $timenow);
1467          quiz_attempt_save_started($quizobj, $quba, $attempt);
1468  
1469          // Force abandon.
1470          $quiz->timeclose = $timenow - HOURSECS;
1471          $DB->update_record('quiz', $quiz);
1472  
1473          $result = mod_quiz_external::process_attempt($attempt->id, array());
1474          $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1475          $this->assertEquals(quiz_attempt::ABANDONED, $result['state']);
1476  
1477      }
1478  
1479      /**
1480       * Test validate_attempt_review
1481       */
1482      public function test_validate_attempt_review() {
1483          global $DB;
1484  
1485          // Create a new quiz with one attempt started.
1486          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
1487  
1488          $this->setUser($this->student);
1489  
1490          // Invalid attempt, invalid id.
1491          try {
1492              $params = array('attemptid' => -1);
1493              testable_mod_quiz_external::validate_attempt_review($params);
1494              $this->fail('Exception expected due invalid id.');
1495          } catch (\dml_missing_record_exception $e) {
1496              $this->assertEquals('invalidrecord', $e->errorcode);
1497          }
1498  
1499          // Invalid attempt, not closed.
1500          try {
1501              $params = array('attemptid' => $attempt->id);
1502              testable_mod_quiz_external::validate_attempt_review($params);
1503              $this->fail('Exception expected due not closed attempt.');
1504          } catch (\moodle_quiz_exception $e) {
1505              $this->assertEquals('attemptclosed', $e->errorcode);
1506          }
1507  
1508          // Test ok case (finished attempt).
1509          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true);
1510  
1511          $params = array('attemptid' => $attempt->id);
1512          testable_mod_quiz_external::validate_attempt_review($params);
1513  
1514          // Teacher should be able to view the review of one student's attempt.
1515          $this->setUser($this->teacher);
1516          testable_mod_quiz_external::validate_attempt_review($params);
1517  
1518          // We should not see other students attempts.
1519          $anotherstudent = self::getDataGenerator()->create_user();
1520          $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
1521  
1522          $this->setUser($anotherstudent);
1523          try {
1524              $params = array('attemptid' => $attempt->id);
1525              testable_mod_quiz_external::validate_attempt_review($params);
1526              $this->fail('Exception expected due missing permissions.');
1527          } catch (\moodle_quiz_exception $e) {
1528              $this->assertEquals('noreviewattempt', $e->errorcode);
1529          }
1530      }
1531  
1532  
1533      /**
1534       * Test get_attempt_review
1535       */
1536      public function test_get_attempt_review() {
1537          global $DB;
1538  
1539          // Create a new quiz with two questions and one attempt finished.
1540          list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true);
1541  
1542          // Add feedback to the quiz.
1543          $feedback = new \stdClass();
1544          $feedback->quizid = $quiz->id;
1545          $feedback->feedbacktext = 'Feedback text 1';
1546          $feedback->feedbacktextformat = 1;
1547          $feedback->mingrade = 49;
1548          $feedback->maxgrade = 100;
1549          $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1550  
1551          $feedback->feedbacktext = 'Feedback text 2';
1552          $feedback->feedbacktextformat = 1;
1553          $feedback->mingrade = 30;
1554          $feedback->maxgrade = 48;
1555          $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1556  
1557          $result = mod_quiz_external::get_attempt_review($attempt->id);
1558          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
1559  
1560          // Two questions, one completed and correct, the other gave up.
1561          $this->assertEquals(50, $result['grade']);
1562          $this->assertEquals(1, $result['attempt']['attempt']);
1563          $this->assertEquals('finished', $result['attempt']['state']);
1564          $this->assertEquals(1, $result['attempt']['sumgrades']);
1565          $this->assertCount(2, $result['questions']);
1566          $this->assertEquals('gradedright', $result['questions'][0]['state']);
1567          $this->assertEquals(1, $result['questions'][0]['slot']);
1568          $this->assertEquals('gaveup', $result['questions'][1]['state']);
1569          $this->assertEquals(2, $result['questions'][1]['slot']);
1570  
1571          $this->assertCount(1, $result['additionaldata']);
1572          $this->assertEquals('feedback', $result['additionaldata'][0]['id']);
1573          $this->assertEquals('Feedback', $result['additionaldata'][0]['title']);
1574          $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']);
1575  
1576          // Only first page.
1577          $result = mod_quiz_external::get_attempt_review($attempt->id, 0);
1578          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
1579  
1580          $this->assertEquals(50, $result['grade']);
1581          $this->assertEquals(1, $result['attempt']['attempt']);
1582          $this->assertEquals('finished', $result['attempt']['state']);
1583          $this->assertEquals(1, $result['attempt']['sumgrades']);
1584          $this->assertCount(1, $result['questions']);
1585          $this->assertEquals('gradedright', $result['questions'][0]['state']);
1586          $this->assertEquals(1, $result['questions'][0]['slot']);
1587  
1588           $this->assertCount(1, $result['additionaldata']);
1589          $this->assertEquals('feedback', $result['additionaldata'][0]['id']);
1590          $this->assertEquals('Feedback', $result['additionaldata'][0]['title']);
1591          $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']);
1592  
1593      }
1594  
1595      /**
1596       * Test test_view_attempt
1597       */
1598      public function test_view_attempt() {
1599          global $DB;
1600  
1601          // Create a new quiz with two questions and one attempt started.
1602          list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false);
1603  
1604          // Test user with full capabilities.
1605          $this->setUser($this->student);
1606  
1607          // Trigger and capture the event.
1608          $sink = $this->redirectEvents();
1609  
1610          $result = mod_quiz_external::view_attempt($attempt->id, 0);
1611          $result = \external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result);
1612          $this->assertTrue($result['status']);
1613  
1614          $events = $sink->get_events();
1615          $this->assertCount(1, $events);
1616          $event = array_shift($events);
1617  
1618          // Checking that the event contains the expected values.
1619          $this->assertInstanceOf('\mod_quiz\event\attempt_viewed', $event);
1620          $this->assertEquals($context, $event->get_context());
1621          $this->assertEventContextNotUsed($event);
1622          $this->assertNotEmpty($event->get_name());
1623  
1624          // Now, force the quiz with QUIZ_NAVMETHOD_SEQ (sequencial) navigation method.
1625          $DB->set_field('quiz', 'navmethod', QUIZ_NAVMETHOD_SEQ, array('id' => $quiz->id));
1626          // Quiz requiring preflightdata.
1627          $DB->set_field('quiz', 'password', 'abcdef', array('id' => $quiz->id));
1628          $preflightdata = array(array("name" => "quizpassword", "value" => 'abcdef'));
1629  
1630          // See next page.
1631          $result = mod_quiz_external::view_attempt($attempt->id, 1, $preflightdata);
1632          $result = \external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result);
1633          $this->assertTrue($result['status']);
1634  
1635          $events = $sink->get_events();
1636          $this->assertCount(2, $events);
1637  
1638          // Try to go to previous page.
1639          try {
1640              mod_quiz_external::view_attempt($attempt->id, 0);
1641              $this->fail('Exception expected due to try to see a previous page.');
1642          } catch (\moodle_quiz_exception $e) {
1643              $this->assertEquals('Out of sequence access', $e->errorcode);
1644          }
1645  
1646      }
1647  
1648      /**
1649       * Test test_view_attempt_summary
1650       */
1651      public function test_view_attempt_summary() {
1652          global $DB;
1653  
1654          // Create a new quiz with two questions and one attempt started.
1655          list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false);
1656  
1657          // Test user with full capabilities.
1658          $this->setUser($this->student);
1659  
1660          // Trigger and capture the event.
1661          $sink = $this->redirectEvents();
1662  
1663          $result = mod_quiz_external::view_attempt_summary($attempt->id);
1664          $result = \external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result);
1665          $this->assertTrue($result['status']);
1666  
1667          $events = $sink->get_events();
1668          $this->assertCount(1, $events);
1669          $event = array_shift($events);
1670  
1671          // Checking that the event contains the expected values.
1672          $this->assertInstanceOf('\mod_quiz\event\attempt_summary_viewed', $event);
1673          $this->assertEquals($context, $event->get_context());
1674          $moodlequiz = new \moodle_url('/mod/quiz/summary.php', array('attempt' => $attempt->id));
1675          $this->assertEquals($moodlequiz, $event->get_url());
1676          $this->assertEventContextNotUsed($event);
1677          $this->assertNotEmpty($event->get_name());
1678  
1679          // Quiz requiring preflightdata.
1680          $DB->set_field('quiz', 'password', 'abcdef', array('id' => $quiz->id));
1681          $preflightdata = array(array("name" => "quizpassword", "value" => 'abcdef'));
1682  
1683          $result = mod_quiz_external::view_attempt_summary($attempt->id, $preflightdata);
1684          $result = \external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result);
1685          $this->assertTrue($result['status']);
1686  
1687      }
1688  
1689      /**
1690       * Test test_view_attempt_summary
1691       */
1692      public function test_view_attempt_review() {
1693          global $DB;
1694  
1695          // Create a new quiz with two questions and one attempt finished.
1696          list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true);
1697  
1698          // Test user with full capabilities.
1699          $this->setUser($this->student);
1700  
1701          // Trigger and capture the event.
1702          $sink = $this->redirectEvents();
1703  
1704          $result = mod_quiz_external::view_attempt_review($attempt->id, 0);
1705          $result = \external_api::clean_returnvalue(mod_quiz_external::view_attempt_review_returns(), $result);
1706          $this->assertTrue($result['status']);
1707  
1708          $events = $sink->get_events();
1709          $this->assertCount(1, $events);
1710          $event = array_shift($events);
1711  
1712          // Checking that the event contains the expected values.
1713          $this->assertInstanceOf('\mod_quiz\event\attempt_reviewed', $event);
1714          $this->assertEquals($context, $event->get_context());
1715          $moodlequiz = new \moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->id));
1716          $this->assertEquals($moodlequiz, $event->get_url());
1717          $this->assertEventContextNotUsed($event);
1718          $this->assertNotEmpty($event->get_name());
1719  
1720      }
1721  
1722      /**
1723       * Test get_quiz_feedback_for_grade
1724       */
1725      public function test_get_quiz_feedback_for_grade() {
1726          global $DB;
1727  
1728          // Add feedback to the quiz.
1729          $feedback = new \stdClass();
1730          $feedback->quizid = $this->quiz->id;
1731          $feedback->feedbacktext = 'Feedback text 1';
1732          $feedback->feedbacktextformat = 1;
1733          $feedback->mingrade = 49;
1734          $feedback->maxgrade = 100;
1735          $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1736          // Add a fake inline image to the feedback text.
1737          $filename = 'shouldbeanimage.jpg';
1738          $filerecordinline = array(
1739              'contextid' => $this->context->id,
1740              'component' => 'mod_quiz',
1741              'filearea'  => 'feedback',
1742              'itemid'    => $feedback->id,
1743              'filepath'  => '/',
1744              'filename'  => $filename,
1745          );
1746          $fs = get_file_storage();
1747          $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
1748  
1749          $feedback->feedbacktext = 'Feedback text 2';
1750          $feedback->feedbacktextformat = 1;
1751          $feedback->mingrade = 30;
1752          $feedback->maxgrade = 49;
1753          $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1754  
1755          $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 50);
1756          $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result);
1757          $this->assertEquals('Feedback text 1', $result['feedbacktext']);
1758          $this->assertEquals($filename, $result['feedbackinlinefiles'][0]['filename']);
1759          $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']);
1760  
1761          $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 30);
1762          $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result);
1763          $this->assertEquals('Feedback text 2', $result['feedbacktext']);
1764          $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']);
1765  
1766          $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 10);
1767          $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result);
1768          $this->assertEquals('', $result['feedbacktext']);
1769          $this->assertEquals(FORMAT_MOODLE, $result['feedbacktextformat']);
1770      }
1771  
1772      /**
1773       * Test get_quiz_access_information
1774       */
1775      public function test_get_quiz_access_information() {
1776          global $DB;
1777  
1778          // Create a new quiz.
1779          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
1780          $data = array('course' => $this->course->id);
1781          $quiz = $quizgenerator->create_instance($data);
1782  
1783          $this->setUser($this->student);
1784  
1785          // Default restrictions (none).
1786          $result = mod_quiz_external::get_quiz_access_information($quiz->id);
1787          $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result);
1788  
1789          $expected = array(
1790              'canattempt' => true,
1791              'canmanage' => false,
1792              'canpreview' => false,
1793              'canreviewmyattempts' => true,
1794              'canviewreports' => false,
1795              'accessrules' => [],
1796              // This rule is always used, even if the quiz has no open or close date.
1797              'activerulenames' => ['quizaccess_openclosedate'],
1798              'preventaccessreasons' => [],
1799              'warnings' => []
1800          );
1801  
1802          $this->assertEquals($expected, $result);
1803  
1804          // Now teacher, different privileges.
1805          $this->setUser($this->teacher);
1806          $result = mod_quiz_external::get_quiz_access_information($quiz->id);
1807          $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result);
1808  
1809          $expected['canmanage'] = true;
1810          $expected['canpreview'] = true;
1811          $expected['canviewreports'] = true;
1812          $expected['canattempt'] = false;
1813          $expected['canreviewmyattempts'] = false;
1814  
1815          $this->assertEquals($expected, $result);
1816  
1817          $this->setUser($this->student);
1818          // Now add some restrictions.
1819          $quiz->timeopen = time() + DAYSECS;
1820          $quiz->timeclose = time() + WEEKSECS;
1821          $quiz->password = '123456';
1822          $DB->update_record('quiz', $quiz);
1823  
1824          $result = mod_quiz_external::get_quiz_access_information($quiz->id);
1825          $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result);
1826  
1827          // Access is limited by time and password, but only the password limit has a description.
1828          $this->assertCount(1, $result['accessrules']);
1829          // Two rule names, password and open/close date.
1830          $this->assertCount(2, $result['activerulenames']);
1831          $this->assertCount(1, $result['preventaccessreasons']);
1832  
1833      }
1834  
1835      /**
1836       * Test get_attempt_access_information
1837       */
1838      public function test_get_attempt_access_information() {
1839          global $DB;
1840  
1841          $this->setAdminUser();
1842  
1843          // Create a new quiz with attempts.
1844          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
1845          $data = array('course' => $this->course->id,
1846                        'sumgrades' => 2);
1847          $quiz = $quizgenerator->create_instance($data);
1848  
1849          // Create some questions.
1850          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
1851  
1852          $cat = $questiongenerator->create_question_category();
1853          $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
1854          quiz_add_quiz_question($question->id, $quiz);
1855  
1856          $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
1857          quiz_add_quiz_question($question->id, $quiz);
1858  
1859          // Add new question types in the category (for the random one).
1860          $question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id));
1861          $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id));
1862  
1863          quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
1864  
1865          $quizobj = quiz::create($quiz->id, $this->student->id);
1866  
1867          // Set grade to pass.
1868          $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
1869                                          'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
1870          $item->gradepass = 80;
1871          $item->update();
1872  
1873          $this->setUser($this->student);
1874  
1875          // Default restrictions (none).
1876          $result = mod_quiz_external::get_attempt_access_information($quiz->id);
1877          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result);
1878  
1879          $expected = array(
1880              'isfinished' => false,
1881              'preventnewattemptreasons' => [],
1882              'warnings' => []
1883          );
1884  
1885          $this->assertEquals($expected, $result);
1886  
1887          // Limited attempts.
1888          $quiz->attempts = 1;
1889          $DB->update_record('quiz', $quiz);
1890  
1891          // Now, do one attempt.
1892          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1893          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1894  
1895          $timenow = time();
1896          $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
1897          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
1898          quiz_attempt_save_started($quizobj, $quba, $attempt);
1899  
1900          // Process some responses from the student.
1901          $attemptobj = quiz_attempt::create($attempt->id);
1902          $tosubmit = array(1 => array('answer' => '3.14'));
1903          $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
1904  
1905          // Finish the attempt.
1906          $attemptobj = quiz_attempt::create($attempt->id);
1907          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
1908          $attemptobj->process_finish($timenow, false);
1909  
1910          // Can we start a new attempt? We shall not!
1911          $result = mod_quiz_external::get_attempt_access_information($quiz->id, $attempt->id);
1912          $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result);
1913  
1914          // Now new attemps allowed.
1915          $this->assertCount(1, $result['preventnewattemptreasons']);
1916          $this->assertFalse($result['ispreflightcheckrequired']);
1917          $this->assertEquals(get_string('nomoreattempts', 'quiz'), $result['preventnewattemptreasons'][0]);
1918  
1919      }
1920  
1921      /**
1922       * Test get_quiz_required_qtypes
1923       */
1924      public function test_get_quiz_required_qtypes() {
1925          $this->setAdminUser();
1926  
1927          // Create a new quiz.
1928          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
1929          $data = array('course' => $this->course->id);
1930          $quiz = $quizgenerator->create_instance($data);
1931  
1932          // Create some questions.
1933          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
1934  
1935          $cat = $questiongenerator->create_question_category();
1936          $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
1937          quiz_add_quiz_question($question->id, $quiz);
1938  
1939          $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
1940          quiz_add_quiz_question($question->id, $quiz);
1941  
1942          // Add new question types in the category (for the random one).
1943          $question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id));
1944          $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id));
1945  
1946          quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
1947  
1948          $this->setUser($this->student);
1949  
1950          $result = mod_quiz_external::get_quiz_required_qtypes($quiz->id);
1951          $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_required_qtypes_returns(), $result);
1952  
1953          $expected = array(
1954              'questiontypes' => ['essay', 'numerical', 'random', 'shortanswer', 'truefalse'],
1955              'warnings' => []
1956          );
1957  
1958          $this->assertEquals($expected, $result);
1959  
1960      }
1961  
1962      /**
1963       * Test that a sequential navigation quiz is not allowing to see questions in advance except if reviewing
1964       */
1965      public function test_sequential_navigation_view_attempt() {
1966          // Test user with full capabilities.
1967          $quiz = $this->prepare_sequential_quiz();
1968          $attemptobj = $this->create_quiz_attempt_object($quiz);
1969          $this->setUser($this->student);
1970          // Check out of sequence access for view.
1971          $this->assertNotEmpty(mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 0, []));
1972          try {
1973              mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 3, []);
1974              $this->fail('Exception expected due to out of sequence access.');
1975          } catch (\moodle_exception $e) {
1976              $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
1977          }
1978      }
1979  
1980      /**
1981       * Test that a sequential navigation quiz is not allowing to see questions in advance for a student
1982       */
1983      public function test_sequential_navigation_attempt_summary() {
1984          // Test user with full capabilities.
1985          $quiz = $this->prepare_sequential_quiz();
1986          $attemptobj = $this->create_quiz_attempt_object($quiz);
1987          $this->setUser($this->student);
1988          // Check that we do not return other questions than the one currently viewed.
1989          $result = mod_quiz_external::get_attempt_summary($attemptobj->get_attemptid());
1990          $this->assertCount(1, $result['questions']);
1991          $this->assertStringContainsString('Question (1)', $result['questions'][0]['html']);
1992      }
1993  
1994      /**
1995       * Test that a sequential navigation quiz is not allowing to see questions in advance for student
1996       */
1997      public function test_sequential_navigation_get_attempt_data() {
1998          // Test user with full capabilities.
1999          $quiz = $this->prepare_sequential_quiz();
2000          $attemptobj = $this->create_quiz_attempt_object($quiz);
2001          $this->setUser($this->student);
2002          // Test invalid instance id.
2003          try {
2004              mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 2);
2005              $this->fail('Exception expected due to out of sequence access.');
2006          } catch (\moodle_exception $e) {
2007              $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
2008          }
2009          // Now we moved to page 1, we should see page 2 and 1 but not 0 or 3.
2010          $attemptobj->set_currentpage(1);
2011          // Test invalid instance id.
2012          try {
2013              mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 0);
2014              $this->fail('Exception expected due to out of sequence access.');
2015          } catch (\moodle_exception $e) {
2016              $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
2017          }
2018  
2019          try {
2020              mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 3);
2021              $this->fail('Exception expected due to out of sequence access.');
2022          } catch (\moodle_exception $e) {
2023              $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
2024          }
2025  
2026          // Now we can see page 1.
2027          $result = mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 1);
2028          $this->assertCount(1, $result['questions']);
2029          $this->assertStringContainsString('Question (2)', $result['questions'][0]['html']);
2030      }
2031  
2032      /**
2033       * Prepare quiz for sequential navigation tests
2034       *
2035       * @return quiz
2036       */
2037      private function prepare_sequential_quiz() {
2038          // Create a new quiz with 5 questions and one attempt started.
2039          // Create a new quiz with attempts.
2040          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
2041          $data = [
2042              'course' => $this->course->id,
2043              'sumgrades' => 2,
2044              'preferredbehaviour' => 'deferredfeedback',
2045              'navmethod' => QUIZ_NAVMETHOD_SEQ
2046          ];
2047          $quiz = $quizgenerator->create_instance($data);
2048  
2049          // Now generate the questions.
2050          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
2051          $cat = $questiongenerator->create_question_category();
2052          for ($pageindex = 1; $pageindex <= 5; $pageindex++) {
2053              $question = $questiongenerator->create_question('truefalse', null, [
2054                  'category' => $cat->id,
2055                  'questiontext' => ['text' => "Question ($pageindex)"]
2056              ]);
2057              quiz_add_quiz_question($question->id, $quiz, $pageindex);
2058          }
2059  
2060          $quizobj = quiz::create($quiz->id, $this->student->id);
2061          // Set grade to pass.
2062          $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
2063              'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
2064          $item->gradepass = 80;
2065          $item->update();
2066          return $quizobj;
2067      }
2068  
2069      /**
2070       * Create question attempt
2071       *
2072       * @param quiz $quizobj
2073       * @param int|null $userid
2074       * @param bool|null $ispreview
2075       * @return quiz_attempt
2076       * @throws \moodle_exception
2077       */
2078      private function create_quiz_attempt_object($quizobj, $userid = null, $ispreview = false) {
2079          global $USER;
2080          $timenow = time();
2081          // Now, do one attempt.
2082          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
2083          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
2084          $attemptnumber = 1;
2085          if (!empty($USER->id)) {
2086              $attemptnumber = count(quiz_get_user_attempts($quizobj->get_quizid(), $USER->id)) + 1;
2087          }
2088          $attempt = quiz_create_attempt($quizobj, $attemptnumber, false, $timenow, $ispreview, $userid ?? $this->student->id);
2089          quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow);
2090          quiz_attempt_save_started($quizobj, $quba, $attempt);
2091          $attemptobj = quiz_attempt::create($attempt->id);
2092          return $attemptobj;
2093      }
2094  }