Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310]

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