[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/mod/quiz/tests/ -> external_test.php (source)

   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() {
  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          $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
  99          $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
 100      }
 101  
 102      /**
 103       * Create a quiz with questions including a started or finished attempt optionally
 104       *
 105       * @param  boolean $startattempt whether to start a new attempt
 106       * @param  boolean $finishattempt whether to finish the new attempt
 107       * @return array array containing the quiz, context and the attempt
 108       */
 109      private function create_quiz_with_questions($startattempt = false, $finishattempt = false) {
 110  
 111          // Create a new quiz with attempts.
 112          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 113          $data = array('course' => $this->course->id,
 114                        'sumgrades' => 2);
 115          $quiz = $quizgenerator->create_instance($data);
 116          $context = context_module::instance($quiz->cmid);
 117  
 118          // Create a couple of questions.
 119          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 120  
 121          $cat = $questiongenerator->create_question_category();
 122          $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
 123          quiz_add_quiz_question($question->id, $quiz);
 124          $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
 125          quiz_add_quiz_question($question->id, $quiz);
 126  
 127          $quizobj = quiz::create($quiz->id, $this->student->id);
 128  
 129          // Set grade to pass.
 130          $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
 131                                          'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
 132          $item->gradepass = 80;
 133          $item->update();
 134  
 135          if ($startattempt or $finishattempt) {
 136              // Now, do one attempt.
 137              $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 138              $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 139  
 140              $timenow = time();
 141              $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
 142              quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 143              quiz_attempt_save_started($quizobj, $quba, $attempt);
 144              $attemptobj = quiz_attempt::create($attempt->id);
 145  
 146              if ($finishattempt) {
 147                  // Process some responses from the student.
 148                  $tosubmit = array(1 => array('answer' => '3.14'));
 149                  $attemptobj->process_submitted_actions(time(), false, $tosubmit);
 150  
 151                  // Finish the attempt.
 152                  $attemptobj->process_finish(time(), false);
 153              }
 154              return array($quiz, $context, $quizobj, $attempt, $attemptobj, $quba);
 155          } else {
 156              return array($quiz, $context, $quizobj);
 157          }
 158  
 159      }
 160  
 161      /*
 162       * Test get quizzes by courses
 163       */
 164      public function test_mod_quiz_get_quizzes_by_courses() {
 165          global $DB;
 166  
 167          // Create additional course.
 168          $course2 = self::getDataGenerator()->create_course();
 169  
 170          // Second quiz.
 171          $record = new stdClass();
 172          $record->course = $course2->id;
 173          $quiz2 = self::getDataGenerator()->create_module('quiz', $record);
 174  
 175          // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
 176          $enrol = enrol_get_plugin('manual');
 177          $enrolinstances = enrol_get_instances($course2->id, true);
 178          foreach ($enrolinstances as $courseenrolinstance) {
 179              if ($courseenrolinstance->enrol == "manual") {
 180                  $instance2 = $courseenrolinstance;
 181                  break;
 182              }
 183          }
 184          $enrol->enrol_user($instance2, $this->student->id, $this->studentrole->id);
 185  
 186          self::setUser($this->student);
 187  
 188          $returndescription = mod_quiz_external::get_quizzes_by_courses_returns();
 189  
 190          // Create what we expect to be returned when querying the two courses.
 191          // First for the student user.
 192          $allusersfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'timeopen',
 193                                  'timeclose', 'grademethod', 'section', 'visible', 'groupmode', 'groupingid');
 194          $userswithaccessfields = array('timelimit', 'attempts', 'attemptonlast', 'grademethod', 'decimalpoints',
 195                                          'questiondecimalpoints', 'reviewattempt', 'reviewcorrectness', 'reviewmarks',
 196                                          'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer',
 197                                          'reviewoverallfeedback', 'questionsperpage', 'navmethod', 'sumgrades', 'grade',
 198                                          'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks',
 199                                          'completionattemptsexhausted', 'completionpass', 'autosaveperiod', 'hasquestions',
 200                                          'hasfeedback', 'overduehandling', 'graceperiod', 'preferredbehaviour', 'canredoquestions');
 201          $managerfields = array('shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet');
 202  
 203          // Add expected coursemodule and other data.
 204          $quiz1 = $this->quiz;
 205          $quiz1->coursemodule = $quiz1->cmid;
 206          $quiz1->introformat = 1;
 207          $quiz1->section = 0;
 208          $quiz1->visible = true;
 209          $quiz1->groupmode = 0;
 210          $quiz1->groupingid = 0;
 211          $quiz1->hasquestions = 0;
 212          $quiz1->hasfeedback = 0;
 213          $quiz1->autosaveperiod = get_config('quiz', 'autosaveperiod');
 214          $quiz1->introfiles = [];
 215  
 216          $quiz2->coursemodule = $quiz2->cmid;
 217          $quiz2->introformat = 1;
 218          $quiz2->section = 0;
 219          $quiz2->visible = true;
 220          $quiz2->groupmode = 0;
 221          $quiz2->groupingid = 0;
 222          $quiz2->hasquestions = 0;
 223          $quiz2->hasfeedback = 0;
 224          $quiz2->autosaveperiod = get_config('quiz', 'autosaveperiod');
 225          $quiz2->introfiles = [];
 226  
 227          foreach (array_merge($allusersfields, $userswithaccessfields) as $field) {
 228              $expected1[$field] = $quiz1->{$field};
 229              $expected2[$field] = $quiz2->{$field};
 230          }
 231  
 232          $expectedquizzes = array($expected2, $expected1);
 233  
 234          // Call the external function passing course ids.
 235          $result = mod_quiz_external::get_quizzes_by_courses(array($course2->id, $this->course->id));
 236          $result = external_api::clean_returnvalue($returndescription, $result);
 237  
 238          $this->assertEquals($expectedquizzes, $result['quizzes']);
 239          $this->assertCount(0, $result['warnings']);
 240  
 241          // Call the external function without passing course id.
 242          $result = mod_quiz_external::get_quizzes_by_courses();
 243          $result = external_api::clean_returnvalue($returndescription, $result);
 244          $this->assertEquals($expectedquizzes, $result['quizzes']);
 245          $this->assertCount(0, $result['warnings']);
 246  
 247          // Unenrol user from second course and alter expected quizzes.
 248          $enrol->unenrol_user($instance2, $this->student->id);
 249          array_shift($expectedquizzes);
 250  
 251          // Call the external function without passing course id.
 252          $result = mod_quiz_external::get_quizzes_by_courses();
 253          $result = external_api::clean_returnvalue($returndescription, $result);
 254          $this->assertEquals($expectedquizzes, $result['quizzes']);
 255  
 256          // Call for the second course we unenrolled the user from, expected warning.
 257          $result = mod_quiz_external::get_quizzes_by_courses(array($course2->id));
 258          $this->assertCount(1, $result['warnings']);
 259          $this->assertEquals('1', $result['warnings'][0]['warningcode']);
 260          $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
 261  
 262          // Now, try as a teacher for getting all the additional fields.
 263          self::setUser($this->teacher);
 264  
 265          foreach ($managerfields as $field) {
 266              $expectedquizzes[0][$field] = $quiz1->{$field};
 267          }
 268  
 269          $result = mod_quiz_external::get_quizzes_by_courses();
 270          $result = external_api::clean_returnvalue($returndescription, $result);
 271          $this->assertEquals($expectedquizzes, $result['quizzes']);
 272  
 273          // Admin also should get all the information.
 274          self::setAdminUser();
 275  
 276          $result = mod_quiz_external::get_quizzes_by_courses(array($this->course->id));
 277          $result = external_api::clean_returnvalue($returndescription, $result);
 278          $this->assertEquals($expectedquizzes, $result['quizzes']);
 279  
 280          // Now, prevent access.
 281          $enrol->enrol_user($instance2, $this->student->id);
 282  
 283          self::setUser($this->student);
 284  
 285          $quiz2->timeclose = time() - DAYSECS;
 286          $DB->update_record('quiz', $quiz2);
 287  
 288          $result = mod_quiz_external::get_quizzes_by_courses();
 289          $result = external_api::clean_returnvalue($returndescription, $result);
 290          $this->assertCount(2, $result['quizzes']);
 291          // We only see a limited set of fields.
 292          $this->assertCount(4, $result['quizzes'][0]);
 293          $this->assertEquals($quiz2->id, $result['quizzes'][0]['id']);
 294          $this->assertEquals($quiz2->coursemodule, $result['quizzes'][0]['coursemodule']);
 295          $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']);
 296          $this->assertEquals($quiz2->name, $result['quizzes'][0]['name']);
 297          $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']);
 298  
 299          $this->assertFalse(isset($result['quizzes'][0]['timelimit']));
 300  
 301      }
 302  
 303      /**
 304       * Test test_view_quiz
 305       */
 306      public function test_view_quiz() {
 307          global $DB;
 308  
 309          // Test invalid instance id.
 310          try {
 311              mod_quiz_external::view_quiz(0);
 312              $this->fail('Exception expected due to invalid mod_quiz instance id.');
 313          } catch (moodle_exception $e) {
 314              $this->assertEquals('invalidrecord', $e->errorcode);
 315          }
 316  
 317          // Test not-enrolled user.
 318          $usernotenrolled = self::getDataGenerator()->create_user();
 319          $this->setUser($usernotenrolled);
 320          try {
 321              mod_quiz_external::view_quiz($this->quiz->id);
 322              $this->fail('Exception expected due to not enrolled user.');
 323          } catch (moodle_exception $e) {
 324              $this->assertEquals('requireloginerror', $e->errorcode);
 325          }
 326  
 327          // Test user with full capabilities.
 328          $this->setUser($this->student);
 329  
 330          // Trigger and capture the event.
 331          $sink = $this->redirectEvents();
 332  
 333          $result = mod_quiz_external::view_quiz($this->quiz->id);
 334          $result = external_api::clean_returnvalue(mod_quiz_external::view_quiz_returns(), $result);
 335          $this->assertTrue($result['status']);
 336  
 337          $events = $sink->get_events();
 338          $this->assertCount(1, $events);
 339          $event = array_shift($events);
 340  
 341          // Checking that the event contains the expected values.
 342          $this->assertInstanceOf('\mod_quiz\event\course_module_viewed', $event);
 343          $this->assertEquals($this->context, $event->get_context());
 344          $moodlequiz = new \moodle_url('/mod/quiz/view.php', array('id' => $this->cm->id));
 345          $this->assertEquals($moodlequiz, $event->get_url());
 346          $this->assertEventContextNotUsed($event);
 347          $this->assertNotEmpty($event->get_name());
 348  
 349          // Test user with no capabilities.
 350          // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
 351          assign_capability('mod/quiz:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
 352          // Empty all the caches that may be affected  by this change.
 353          accesslib_clear_all_caches_for_unit_testing();
 354          course_modinfo::clear_instance_cache();
 355  
 356          try {
 357              mod_quiz_external::view_quiz($this->quiz->id);
 358              $this->fail('Exception expected due to missing capability.');
 359          } catch (moodle_exception $e) {
 360              $this->assertEquals('requireloginerror', $e->errorcode);
 361          }
 362  
 363      }
 364  
 365      /**
 366       * Test get_user_attempts
 367       */
 368      public function test_get_user_attempts() {
 369  
 370          // Create a quiz with one attempt finished.
 371          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true);
 372  
 373          $this->setUser($this->student);
 374          $result = mod_quiz_external::get_user_attempts($quiz->id);
 375          $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 376  
 377          $this->assertCount(1, $result['attempts']);
 378          $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
 379          $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
 380          $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
 381          $this->assertEquals(1, $result['attempts'][0]['attempt']);
 382  
 383          // Test filters. Only finished.
 384          $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'finished', false);
 385          $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 386  
 387          $this->assertCount(1, $result['attempts']);
 388          $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
 389  
 390          // Test filters. All attempts.
 391          $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
 392          $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 393  
 394          $this->assertCount(1, $result['attempts']);
 395          $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
 396  
 397          // Test filters. Unfinished.
 398          $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
 399          $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 400  
 401          $this->assertCount(0, $result['attempts']);
 402  
 403          // Start a new attempt, but not finish it.
 404          $timenow = time();
 405          $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
 406          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 407          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 408  
 409          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 410          quiz_attempt_save_started($quizobj, $quba, $attempt);
 411  
 412          // Test filters. All attempts.
 413          $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
 414          $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 415  
 416          $this->assertCount(2, $result['attempts']);
 417  
 418          // Test filters. Unfinished.
 419          $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
 420          $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 421  
 422          $this->assertCount(1, $result['attempts']);
 423  
 424          // Test manager can see user attempts.
 425          $this->setUser($this->teacher);
 426          $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id);
 427          $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 428  
 429          $this->assertCount(1, $result['attempts']);
 430          $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
 431  
 432          $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'all');
 433          $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
 434  
 435          $this->assertCount(2, $result['attempts']);
 436          $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
 437  
 438          // Invalid parameters.
 439          try {
 440              mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'INVALID_PARAMETER');
 441              $this->fail('Exception expected due to missing capability.');
 442          } catch (invalid_parameter_exception $e) {
 443              $this->assertEquals('invalidparameter', $e->errorcode);
 444          }
 445      }
 446  
 447      /**
 448       * Test get_user_best_grade
 449       */
 450      public function test_get_user_best_grade() {
 451          global $DB;
 452  
 453          $this->setUser($this->student);
 454  
 455          $result = mod_quiz_external::get_user_best_grade($this->quiz->id);
 456          $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
 457  
 458          // No grades yet.
 459          $this->assertFalse($result['hasgrade']);
 460          $this->assertTrue(!isset($result['grade']));
 461  
 462          $grade = new stdClass();
 463          $grade->quiz = $this->quiz->id;
 464          $grade->userid = $this->student->id;
 465          $grade->grade = 8.9;
 466          $grade->timemodified = time();
 467          $grade->id = $DB->insert_record('quiz_grades', $grade);
 468  
 469          $result = mod_quiz_external::get_user_best_grade($this->quiz->id);
 470          $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
 471  
 472          // Now I have grades.
 473          $this->assertTrue($result['hasgrade']);
 474          $this->assertEquals(8.9, $result['grade']);
 475  
 476          // We should not see other users grades.
 477          $anotherstudent = self::getDataGenerator()->create_user();
 478          $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
 479  
 480          try {
 481              mod_quiz_external::get_user_best_grade($this->quiz->id, $anotherstudent->id);
 482              $this->fail('Exception expected due to missing capability.');
 483          } catch (required_capability_exception $e) {
 484              $this->assertEquals('nopermissions', $e->errorcode);
 485          }
 486  
 487          // Teacher must be able to see student grades.
 488          $this->setUser($this->teacher);
 489  
 490          $result = mod_quiz_external::get_user_best_grade($this->quiz->id, $this->student->id);
 491          $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
 492  
 493          $this->assertTrue($result['hasgrade']);
 494          $this->assertEquals(8.9, $result['grade']);
 495  
 496          // Invalid user.
 497          try {
 498              mod_quiz_external::get_user_best_grade($this->quiz->id, -1);
 499              $this->fail('Exception expected due to missing capability.');
 500          } catch (dml_missing_record_exception $e) {
 501              $this->assertEquals('invaliduser', $e->errorcode);
 502          }
 503  
 504          // Remove the created data.
 505          $DB->delete_records('quiz_grades', array('id' => $grade->id));
 506  
 507      }
 508      /**
 509       * Test get_combined_review_options.
 510       * This is a basic test, this is already tested in mod_quiz_display_options_testcase.
 511       */
 512      public function test_get_combined_review_options() {
 513          global $DB;
 514  
 515          // Create a new quiz with attempts.
 516          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 517          $data = array('course' => $this->course->id,
 518                        'sumgrades' => 1);
 519          $quiz = $quizgenerator->create_instance($data);
 520  
 521          // Create a couple of questions.
 522          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 523  
 524          $cat = $questiongenerator->create_question_category();
 525          $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
 526          quiz_add_quiz_question($question->id, $quiz);
 527  
 528          $quizobj = quiz::create($quiz->id, $this->student->id);
 529  
 530          // Set grade to pass.
 531          $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
 532                                          'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
 533          $item->gradepass = 80;
 534          $item->update();
 535  
 536          // Start the passing attempt.
 537          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 538          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 539  
 540          $timenow = time();
 541          $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
 542          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 543          quiz_attempt_save_started($quizobj, $quba, $attempt);
 544  
 545          $this->setUser($this->student);
 546  
 547          $result = mod_quiz_external::get_combined_review_options($quiz->id);
 548          $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
 549  
 550          // Expected values.
 551          $expected = array(
 552              "someoptions" => array(
 553                  array("name" => "feedback", "value" => 1),
 554                  array("name" => "generalfeedback", "value" => 1),
 555                  array("name" => "rightanswer", "value" => 1),
 556                  array("name" => "overallfeedback", "value" => 0),
 557                  array("name" => "marks", "value" => 2),
 558              ),
 559              "alloptions" => array(
 560                  array("name" => "feedback", "value" => 1),
 561                  array("name" => "generalfeedback", "value" => 1),
 562                  array("name" => "rightanswer", "value" => 1),
 563                  array("name" => "overallfeedback", "value" => 0),
 564                  array("name" => "marks", "value" => 2),
 565              ),
 566              "warnings" => [],
 567          );
 568  
 569          $this->assertEquals($expected, $result);
 570  
 571          // Now, finish the attempt.
 572          $attemptobj = quiz_attempt::create($attempt->id);
 573          $attemptobj->process_finish($timenow, false);
 574  
 575          $expected = array(
 576              "someoptions" => array(
 577                  array("name" => "feedback", "value" => 1),
 578                  array("name" => "generalfeedback", "value" => 1),
 579                  array("name" => "rightanswer", "value" => 1),
 580                  array("name" => "overallfeedback", "value" => 1),
 581                  array("name" => "marks", "value" => 2),
 582              ),
 583              "alloptions" => array(
 584                  array("name" => "feedback", "value" => 1),
 585                  array("name" => "generalfeedback", "value" => 1),
 586                  array("name" => "rightanswer", "value" => 1),
 587                  array("name" => "overallfeedback", "value" => 1),
 588                  array("name" => "marks", "value" => 2),
 589              ),
 590              "warnings" => [],
 591          );
 592  
 593          // We should see now the overall feedback.
 594          $result = mod_quiz_external::get_combined_review_options($quiz->id);
 595          $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
 596          $this->assertEquals($expected, $result);
 597  
 598          // Start a new attempt, but not finish it.
 599          $timenow = time();
 600          $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
 601          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 602          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 603          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 604          quiz_attempt_save_started($quizobj, $quba, $attempt);
 605  
 606          $expected = array(
 607              "someoptions" => array(
 608                  array("name" => "feedback", "value" => 1),
 609                  array("name" => "generalfeedback", "value" => 1),
 610                  array("name" => "rightanswer", "value" => 1),
 611                  array("name" => "overallfeedback", "value" => 1),
 612                  array("name" => "marks", "value" => 2),
 613              ),
 614              "alloptions" => array(
 615                  array("name" => "feedback", "value" => 1),
 616                  array("name" => "generalfeedback", "value" => 1),
 617                  array("name" => "rightanswer", "value" => 1),
 618                  array("name" => "overallfeedback", "value" => 0),
 619                  array("name" => "marks", "value" => 2),
 620              ),
 621              "warnings" => [],
 622          );
 623  
 624          $result = mod_quiz_external::get_combined_review_options($quiz->id);
 625          $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
 626          $this->assertEquals($expected, $result);
 627  
 628          // Teacher, for see student options.
 629          $this->setUser($this->teacher);
 630  
 631          $result = mod_quiz_external::get_combined_review_options($quiz->id, $this->student->id);
 632          $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
 633  
 634          $this->assertEquals($expected, $result);
 635  
 636          // Invalid user.
 637          try {
 638              mod_quiz_external::get_combined_review_options($quiz->id, -1);
 639              $this->fail('Exception expected due to missing capability.');
 640          } catch (dml_missing_record_exception $e) {
 641              $this->assertEquals('invaliduser', $e->errorcode);
 642          }
 643      }
 644  
 645      /**
 646       * Test start_attempt
 647       */
 648      public function test_start_attempt() {
 649          global $DB;
 650  
 651          // Create a new quiz with questions.
 652          list($quiz, $context, $quizobj) = $this->create_quiz_with_questions();
 653  
 654          $this->setUser($this->student);
 655  
 656          // Try to open attempt in closed quiz.
 657          $quiz->timeopen = time() - WEEKSECS;
 658          $quiz->timeclose = time() - DAYSECS;
 659          $DB->update_record('quiz', $quiz);
 660          $result = mod_quiz_external::start_attempt($quiz->id);
 661          $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
 662  
 663          $this->assertEquals([], $result['attempt']);
 664          $this->assertCount(1, $result['warnings']);
 665  
 666          // Now with a password.
 667          $quiz->timeopen = 0;
 668          $quiz->timeclose = 0;
 669          $quiz->password = 'abc';
 670          $DB->update_record('quiz', $quiz);
 671  
 672          try {
 673              mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'bad')));
 674              $this->fail('Exception expected due to invalid passwod.');
 675          } catch (moodle_exception $e) {
 676              $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode);
 677          }
 678  
 679          // Now, try everything correct.
 680          $result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc')));
 681          $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
 682  
 683          $this->assertEquals(1, $result['attempt']['attempt']);
 684          $this->assertEquals($this->student->id, $result['attempt']['userid']);
 685          $this->assertEquals($quiz->id, $result['attempt']['quiz']);
 686          $this->assertCount(0, $result['warnings']);
 687          $attemptid = $result['attempt']['id'];
 688  
 689          // We are good, try to start a new attempt now.
 690  
 691          try {
 692              mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc')));
 693              $this->fail('Exception expected due to attempt not finished.');
 694          } catch (moodle_quiz_exception $e) {
 695              $this->assertEquals('attemptstillinprogress', $e->errorcode);
 696          }
 697  
 698          // Finish the started attempt.
 699  
 700          // Process some responses from the student.
 701          $timenow = time();
 702          $attemptobj = quiz_attempt::create($attemptid);
 703          $tosubmit = array(1 => array('answer' => '3.14'));
 704          $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
 705  
 706          // Finish the attempt.
 707          $attemptobj = quiz_attempt::create($attemptid);
 708          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 709          $attemptobj->process_finish($timenow, false);
 710  
 711          // We should be able to start a new attempt.
 712          $result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc')));
 713          $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
 714  
 715          $this->assertEquals(2, $result['attempt']['attempt']);
 716          $this->assertEquals($this->student->id, $result['attempt']['userid']);
 717          $this->assertEquals($quiz->id, $result['attempt']['quiz']);
 718          $this->assertCount(0, $result['warnings']);
 719  
 720          // Test user with no capabilities.
 721          // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
 722          assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id);
 723          // Empty all the caches that may be affected  by this change.
 724          accesslib_clear_all_caches_for_unit_testing();
 725          course_modinfo::clear_instance_cache();
 726  
 727          try {
 728              mod_quiz_external::start_attempt($quiz->id);
 729              $this->fail('Exception expected due to missing capability.');
 730          } catch (required_capability_exception $e) {
 731              $this->assertEquals('nopermissions', $e->errorcode);
 732          }
 733  
 734      }
 735  
 736      /**
 737       * Test validate_attempt
 738       */
 739      public function test_validate_attempt() {
 740          global $DB;
 741  
 742          // Create a new quiz with one attempt started.
 743          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
 744  
 745          $this->setUser($this->student);
 746  
 747          // Invalid attempt.
 748          try {
 749              $params = array('attemptid' => -1, 'page' => 0);
 750              testable_mod_quiz_external::validate_attempt($params);
 751              $this->fail('Exception expected due to invalid attempt id.');
 752          } catch (dml_missing_record_exception $e) {
 753              $this->assertEquals('invalidrecord', $e->errorcode);
 754          }
 755  
 756          // Test OK case.
 757          $params = array('attemptid' => $attempt->id, 'page' => 0);
 758          $result = testable_mod_quiz_external::validate_attempt($params);
 759          $this->assertEquals($attempt->id, $result[0]->get_attempt()->id);
 760          $this->assertEquals([], $result[1]);
 761  
 762          // Test with preflight data.
 763          $quiz->password = 'abc';
 764          $DB->update_record('quiz', $quiz);
 765  
 766          try {
 767              $params = array('attemptid' => $attempt->id, 'page' => 0,
 768                              'preflightdata' => array(array("name" => "quizpassword", "value" => 'bad')));
 769              testable_mod_quiz_external::validate_attempt($params);
 770              $this->fail('Exception expected due to invalid passwod.');
 771          } catch (moodle_exception $e) {
 772              $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode);
 773          }
 774  
 775          // Now, try everything correct.
 776          $params['preflightdata'][0]['value'] = 'abc';
 777          $result = testable_mod_quiz_external::validate_attempt($params);
 778          $this->assertEquals($attempt->id, $result[0]->get_attempt()->id);
 779          $this->assertEquals([], $result[1]);
 780  
 781          // Page out of range.
 782          $DB->update_record('quiz', $quiz);
 783          $params['page'] = 4;
 784          try {
 785              testable_mod_quiz_external::validate_attempt($params);
 786              $this->fail('Exception expected due to page out of range.');
 787          } catch (moodle_quiz_exception $e) {
 788              $this->assertEquals('Invalid page number', $e->errorcode);
 789          }
 790  
 791          $params['page'] = 0;
 792          // Try to open attempt in closed quiz.
 793          $quiz->timeopen = time() - WEEKSECS;
 794          $quiz->timeclose = time() - DAYSECS;
 795          $DB->update_record('quiz', $quiz);
 796  
 797          // This should work, ommit access rules.
 798          testable_mod_quiz_external::validate_attempt($params, false);
 799  
 800          // Get a generic error because prior to checking the dates the attempt is closed.
 801          try {
 802              testable_mod_quiz_external::validate_attempt($params);
 803              $this->fail('Exception expected due to passed dates.');
 804          } catch (moodle_quiz_exception $e) {
 805              $this->assertEquals('attempterror', $e->errorcode);
 806          }
 807  
 808          // Finish the attempt.
 809          $attemptobj = quiz_attempt::create($attempt->id);
 810          $attemptobj->process_finish(time(), false);
 811  
 812          try {
 813              testable_mod_quiz_external::validate_attempt($params, false);
 814              $this->fail('Exception expected due to attempt finished.');
 815          } catch (moodle_quiz_exception $e) {
 816              $this->assertEquals('attemptalreadyclosed', $e->errorcode);
 817          }
 818  
 819          // Test user with no capabilities.
 820          // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
 821          assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id);
 822          // Empty all the caches that may be affected  by this change.
 823          accesslib_clear_all_caches_for_unit_testing();
 824          course_modinfo::clear_instance_cache();
 825  
 826          try {
 827              testable_mod_quiz_external::validate_attempt($params);
 828              $this->fail('Exception expected due to missing permissions.');
 829          } catch (required_capability_exception $e) {
 830              $this->assertEquals('nopermissions', $e->errorcode);
 831          }
 832  
 833          // Now try with a different user.
 834          $this->setUser($this->teacher);
 835  
 836          $params['page'] = 0;
 837          try {
 838              testable_mod_quiz_external::validate_attempt($params);
 839              $this->fail('Exception expected due to not your attempt.');
 840          } catch (moodle_quiz_exception $e) {
 841              $this->assertEquals('notyourattempt', $e->errorcode);
 842          }
 843      }
 844  
 845      /**
 846       * Test get_attempt_data
 847       */
 848      public function test_get_attempt_data() {
 849          global $DB;
 850  
 851          // Create a new quiz with one attempt started.
 852          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
 853  
 854          $quizobj = $attemptobj->get_quizobj();
 855          $quizobj->preload_questions();
 856          $quizobj->load_questions();
 857          $questions = $quizobj->get_questions();
 858  
 859          $this->setUser($this->student);
 860  
 861          // We receive one question per page.
 862          $result = mod_quiz_external::get_attempt_data($attempt->id, 0);
 863          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
 864  
 865          $this->assertEquals($attempt, (object) $result['attempt']);
 866          $this->assertEquals(1, $result['nextpage']);
 867          $this->assertCount(0, $result['messages']);
 868          $this->assertCount(1, $result['questions']);
 869          $this->assertEquals(1, $result['questions'][0]['slot']);
 870          $this->assertEquals(1, $result['questions'][0]['number']);
 871          $this->assertEquals('numerical', $result['questions'][0]['type']);
 872          $this->assertEquals('todo', $result['questions'][0]['state']);
 873          $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
 874          $this->assertFalse($result['questions'][0]['flagged']);
 875          $this->assertEquals(0, $result['questions'][0]['page']);
 876          $this->assertEmpty($result['questions'][0]['mark']);
 877          $this->assertEquals(1, $result['questions'][0]['maxmark']);
 878  
 879          // Now try the last page.
 880          $result = mod_quiz_external::get_attempt_data($attempt->id, 1);
 881          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
 882  
 883          $this->assertEquals($attempt, (object) $result['attempt']);
 884          $this->assertEquals(-1, $result['nextpage']);
 885          $this->assertCount(0, $result['messages']);
 886          $this->assertCount(1, $result['questions']);
 887          $this->assertEquals(2, $result['questions'][0]['slot']);
 888          $this->assertEquals(2, $result['questions'][0]['number']);
 889          $this->assertEquals('numerical', $result['questions'][0]['type']);
 890          $this->assertEquals('todo', $result['questions'][0]['state']);
 891          $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
 892          $this->assertFalse($result['questions'][0]['flagged']);
 893          $this->assertEquals(1, $result['questions'][0]['page']);
 894  
 895          // Finish previous attempt.
 896          $attemptobj->process_finish(time(), false);
 897  
 898          // Change setting and expect two pages.
 899          $quiz->questionsperpage = 4;
 900          $DB->update_record('quiz', $quiz);
 901          quiz_repaginate_questions($quiz->id, $quiz->questionsperpage);
 902  
 903          // Start with new attempt with the new layout.
 904          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 905          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 906  
 907          $timenow = time();
 908          $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
 909          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 910          quiz_attempt_save_started($quizobj, $quba, $attempt);
 911  
 912          // We receive two questions per page.
 913          $result = mod_quiz_external::get_attempt_data($attempt->id, 0);
 914          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
 915          $this->assertCount(2, $result['questions']);
 916          $this->assertEquals(-1, $result['nextpage']);
 917  
 918          // Check questions looks good.
 919          $found = 0;
 920          foreach ($questions as $question) {
 921              foreach ($result['questions'] as $rquestion) {
 922                  if ($rquestion['slot'] == $question->slot) {
 923                      $this->assertTrue(strpos($rquestion['html'], "qid=$question->id") !== false);
 924                      $found++;
 925                  }
 926              }
 927          }
 928          $this->assertEquals(2, $found);
 929  
 930      }
 931  
 932      /**
 933       * Test get_attempt_summary
 934       */
 935      public function test_get_attempt_summary() {
 936  
 937          // Create a new quiz with one attempt started.
 938          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
 939  
 940          $this->setUser($this->student);
 941          $result = mod_quiz_external::get_attempt_summary($attempt->id);
 942          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
 943  
 944          // Check the state, flagged and mark data is correct.
 945          $this->assertEquals('todo', $result['questions'][0]['state']);
 946          $this->assertEquals('todo', $result['questions'][1]['state']);
 947          $this->assertEquals(1, $result['questions'][0]['number']);
 948          $this->assertEquals(2, $result['questions'][1]['number']);
 949          $this->assertFalse($result['questions'][0]['flagged']);
 950          $this->assertFalse($result['questions'][1]['flagged']);
 951          $this->assertEmpty($result['questions'][0]['mark']);
 952          $this->assertEmpty($result['questions'][1]['mark']);
 953  
 954          // Submit a response for the first question.
 955          $tosubmit = array(1 => array('answer' => '3.14'));
 956          $attemptobj->process_submitted_actions(time(), false, $tosubmit);
 957          $result = mod_quiz_external::get_attempt_summary($attempt->id);
 958          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
 959  
 960          // Check it's marked as completed only the first one.
 961          $this->assertEquals('complete', $result['questions'][0]['state']);
 962          $this->assertEquals('todo', $result['questions'][1]['state']);
 963          $this->assertEquals(1, $result['questions'][0]['number']);
 964          $this->assertEquals(2, $result['questions'][1]['number']);
 965          $this->assertFalse($result['questions'][0]['flagged']);
 966          $this->assertFalse($result['questions'][1]['flagged']);
 967          $this->assertEmpty($result['questions'][0]['mark']);
 968          $this->assertEmpty($result['questions'][1]['mark']);
 969  
 970      }
 971  
 972      /**
 973       * Test save_attempt
 974       */
 975      public function test_save_attempt() {
 976  
 977          // Create a new quiz with one attempt started.
 978          list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true);
 979  
 980          // Response for slot 1.
 981          $prefix = $quba->get_field_prefix(1);
 982          $data = array(
 983              array('name' => 'slots', 'value' => 1),
 984              array('name' => $prefix . ':sequencecheck',
 985                      'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
 986              array('name' => $prefix . 'answer', 'value' => 1),
 987          );
 988  
 989          $this->setUser($this->student);
 990  
 991          $result = mod_quiz_external::save_attempt($attempt->id, $data);
 992          $result = external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result);
 993          $this->assertTrue($result['status']);
 994  
 995          // Now, get the summary.
 996          $result = mod_quiz_external::get_attempt_summary($attempt->id);
 997          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
 998  
 999          // Check it's marked as completed only the first one.
1000          $this->assertEquals('complete', $result['questions'][0]['state']);
1001          $this->assertEquals('todo', $result['questions'][1]['state']);
1002          $this->assertEquals(1, $result['questions'][0]['number']);
1003          $this->assertEquals(2, $result['questions'][1]['number']);
1004          $this->assertFalse($result['questions'][0]['flagged']);
1005          $this->assertFalse($result['questions'][1]['flagged']);
1006          $this->assertEmpty($result['questions'][0]['mark']);
1007          $this->assertEmpty($result['questions'][1]['mark']);
1008  
1009          // Now, second slot.
1010          $prefix = $quba->get_field_prefix(2);
1011          $data = array(
1012              array('name' => 'slots', 'value' => 2),
1013              array('name' => $prefix . ':sequencecheck',
1014                      'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
1015              array('name' => $prefix . 'answer', 'value' => 1),
1016          );
1017  
1018          $result = mod_quiz_external::save_attempt($attempt->id, $data);
1019          $result = external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result);
1020          $this->assertTrue($result['status']);
1021  
1022          // Now, get the summary.
1023          $result = mod_quiz_external::get_attempt_summary($attempt->id);
1024          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1025  
1026          // Check it's marked as completed only the first one.
1027          $this->assertEquals('complete', $result['questions'][0]['state']);
1028          $this->assertEquals('complete', $result['questions'][1]['state']);
1029  
1030      }
1031  
1032      /**
1033       * Test process_attempt
1034       */
1035      public function test_process_attempt() {
1036          global $DB;
1037  
1038          // Create a new quiz with two questions and one attempt started.
1039          list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true);
1040  
1041          // Response for slot 1.
1042          $prefix = $quba->get_field_prefix(1);
1043          $data = array(
1044              array('name' => 'slots', 'value' => 1),
1045              array('name' => $prefix . ':sequencecheck',
1046                      'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
1047              array('name' => $prefix . 'answer', 'value' => 1),
1048          );
1049  
1050          $this->setUser($this->student);
1051  
1052          $result = mod_quiz_external::process_attempt($attempt->id, $data);
1053          $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1054          $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']);
1055  
1056          // Now, get the summary.
1057          $result = mod_quiz_external::get_attempt_summary($attempt->id);
1058          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1059  
1060          // Check it's marked as completed only the first one.
1061          $this->assertEquals('complete', $result['questions'][0]['state']);
1062          $this->assertEquals('todo', $result['questions'][1]['state']);
1063          $this->assertEquals(1, $result['questions'][0]['number']);
1064          $this->assertEquals(2, $result['questions'][1]['number']);
1065          $this->assertFalse($result['questions'][0]['flagged']);
1066          $this->assertFalse($result['questions'][1]['flagged']);
1067          $this->assertEmpty($result['questions'][0]['mark']);
1068          $this->assertEmpty($result['questions'][1]['mark']);
1069  
1070          // Now, second slot.
1071          $prefix = $quba->get_field_prefix(2);
1072          $data = array(
1073              array('name' => 'slots', 'value' => 2),
1074              array('name' => $prefix . ':sequencecheck',
1075                      'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
1076              array('name' => $prefix . 'answer', 'value' => 1),
1077              array('name' => $prefix . ':flagged', 'value' => 1),
1078          );
1079  
1080          $result = mod_quiz_external::process_attempt($attempt->id, $data);
1081          $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1082          $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']);
1083  
1084          // Now, get the summary.
1085          $result = mod_quiz_external::get_attempt_summary($attempt->id);
1086          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1087  
1088          // Check it's marked as completed only the first one.
1089          $this->assertEquals('complete', $result['questions'][0]['state']);
1090          $this->assertEquals('complete', $result['questions'][1]['state']);
1091          $this->assertFalse($result['questions'][0]['flagged']);
1092          $this->assertTrue($result['questions'][1]['flagged']);
1093  
1094          // Finish the attempt.
1095          $result = mod_quiz_external::process_attempt($attempt->id, array(), true);
1096          $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1097          $this->assertEquals(quiz_attempt::FINISHED, $result['state']);
1098  
1099          // Start new attempt.
1100          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1101          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1102  
1103          $timenow = time();
1104          $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
1105          quiz_start_new_attempt($quizobj, $quba, $attempt, 2, $timenow);
1106          quiz_attempt_save_started($quizobj, $quba, $attempt);
1107  
1108          // Force grace period, attempt going to overdue.
1109          $quiz->timeclose = $timenow - 10;
1110          $quiz->graceperiod = 60;
1111          $quiz->overduehandling = 'graceperiod';
1112          $DB->update_record('quiz', $quiz);
1113  
1114          $result = mod_quiz_external::process_attempt($attempt->id, array());
1115          $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1116          $this->assertEquals(quiz_attempt::OVERDUE, $result['state']);
1117  
1118          // New attempt.
1119          $timenow = time();
1120          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1121          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1122          $attempt = quiz_create_attempt($quizobj, 3, 2, $timenow, false, $this->student->id);
1123          quiz_start_new_attempt($quizobj, $quba, $attempt, 3, $timenow);
1124          quiz_attempt_save_started($quizobj, $quba, $attempt);
1125  
1126          // Force abandon.
1127          $quiz->timeclose = $timenow - HOURSECS;
1128          $DB->update_record('quiz', $quiz);
1129  
1130          $result = mod_quiz_external::process_attempt($attempt->id, array());
1131          $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1132          $this->assertEquals(quiz_attempt::ABANDONED, $result['state']);
1133  
1134      }
1135  
1136      /**
1137       * Test validate_attempt_review
1138       */
1139      public function test_validate_attempt_review() {
1140          global $DB;
1141  
1142          // Create a new quiz with one attempt started.
1143          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
1144  
1145          $this->setUser($this->student);
1146  
1147          // Invalid attempt, invalid id.
1148          try {
1149              $params = array('attemptid' => -1);
1150              testable_mod_quiz_external::validate_attempt_review($params);
1151              $this->fail('Exception expected due invalid id.');
1152          } catch (dml_missing_record_exception $e) {
1153              $this->assertEquals('invalidrecord', $e->errorcode);
1154          }
1155  
1156          // Invalid attempt, not closed.
1157          try {
1158              $params = array('attemptid' => $attempt->id);
1159              testable_mod_quiz_external::validate_attempt_review($params);
1160              $this->fail('Exception expected due not closed attempt.');
1161          } catch (moodle_quiz_exception $e) {
1162              $this->assertEquals('attemptclosed', $e->errorcode);
1163          }
1164  
1165          // Test ok case (finished attempt).
1166          list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true);
1167  
1168          $params = array('attemptid' => $attempt->id);
1169          testable_mod_quiz_external::validate_attempt_review($params);
1170  
1171          // Teacher should be able to view the review of one student's attempt.
1172          $this->setUser($this->teacher);
1173          testable_mod_quiz_external::validate_attempt_review($params);
1174  
1175          // We should not see other students attempts.
1176          $anotherstudent = self::getDataGenerator()->create_user();
1177          $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
1178  
1179          $this->setUser($anotherstudent);
1180          try {
1181              $params = array('attemptid' => $attempt->id);
1182              testable_mod_quiz_external::validate_attempt_review($params);
1183              $this->fail('Exception expected due missing permissions.');
1184          } catch (moodle_quiz_exception $e) {
1185              $this->assertEquals('noreviewattempt', $e->errorcode);
1186          }
1187      }
1188  
1189  
1190      /**
1191       * Test get_attempt_review
1192       */
1193      public function test_get_attempt_review() {
1194          global $DB;
1195  
1196          // Create a new quiz with two questions and one attempt finished.
1197          list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true);
1198  
1199          // Add feedback to the quiz.
1200          $feedback = new stdClass();
1201          $feedback->quizid = $quiz->id;
1202          $feedback->feedbacktext = 'Feedback text 1';
1203          $feedback->feedbacktextformat = 1;
1204          $feedback->mingrade = 49;
1205          $feedback->maxgrade = 100;
1206          $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1207  
1208          $feedback->feedbacktext = 'Feedback text 2';
1209          $feedback->feedbacktextformat = 1;
1210          $feedback->mingrade = 30;
1211          $feedback->maxgrade = 48;
1212          $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1213  
1214          $result = mod_quiz_external::get_attempt_review($attempt->id);
1215          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
1216  
1217          // Two questions, one completed and correct, the other gave up.
1218          $this->assertEquals(50, $result['grade']);
1219          $this->assertEquals(1, $result['attempt']['attempt']);
1220          $this->assertEquals('finished', $result['attempt']['state']);
1221          $this->assertEquals(1, $result['attempt']['sumgrades']);
1222          $this->assertCount(2, $result['questions']);
1223          $this->assertEquals('gradedright', $result['questions'][0]['state']);
1224          $this->assertEquals(1, $result['questions'][0]['slot']);
1225          $this->assertEquals('gaveup', $result['questions'][1]['state']);
1226          $this->assertEquals(2, $result['questions'][1]['slot']);
1227  
1228          $this->assertCount(1, $result['additionaldata']);
1229          $this->assertEquals('feedback', $result['additionaldata'][0]['id']);
1230          $this->assertEquals('Feedback', $result['additionaldata'][0]['title']);
1231          $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']);
1232  
1233          // Only first page.
1234          $result = mod_quiz_external::get_attempt_review($attempt->id, 0);
1235          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
1236  
1237          $this->assertEquals(50, $result['grade']);
1238          $this->assertEquals(1, $result['attempt']['attempt']);
1239          $this->assertEquals('finished', $result['attempt']['state']);
1240          $this->assertEquals(1, $result['attempt']['sumgrades']);
1241          $this->assertCount(1, $result['questions']);
1242          $this->assertEquals('gradedright', $result['questions'][0]['state']);
1243          $this->assertEquals(1, $result['questions'][0]['slot']);
1244  
1245           $this->assertCount(1, $result['additionaldata']);
1246          $this->assertEquals('feedback', $result['additionaldata'][0]['id']);
1247          $this->assertEquals('Feedback', $result['additionaldata'][0]['title']);
1248          $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']);
1249  
1250      }
1251  
1252      /**
1253       * Test test_view_attempt
1254       */
1255      public function test_view_attempt() {
1256          global $DB;
1257  
1258          // Create a new quiz with two questions and one attempt started.
1259          list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false);
1260  
1261          // Test user with full capabilities.
1262          $this->setUser($this->student);
1263  
1264          // Trigger and capture the event.
1265          $sink = $this->redirectEvents();
1266  
1267          $result = mod_quiz_external::view_attempt($attempt->id, 0);
1268          $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result);
1269          $this->assertTrue($result['status']);
1270  
1271          $events = $sink->get_events();
1272          $this->assertCount(1, $events);
1273          $event = array_shift($events);
1274  
1275          // Checking that the event contains the expected values.
1276          $this->assertInstanceOf('\mod_quiz\event\attempt_viewed', $event);
1277          $this->assertEquals($context, $event->get_context());
1278          $this->assertEventContextNotUsed($event);
1279          $this->assertNotEmpty($event->get_name());
1280  
1281          // Now, force the quiz with QUIZ_NAVMETHOD_SEQ (sequencial) navigation method.
1282          $DB->set_field('quiz', 'navmethod', QUIZ_NAVMETHOD_SEQ, array('id' => $quiz->id));
1283          // Quiz requiring preflightdata.
1284          $DB->set_field('quiz', 'password', 'abcdef', array('id' => $quiz->id));
1285          $preflightdata = array(array("name" => "quizpassword", "value" => 'abcdef'));
1286  
1287          // See next page.
1288          $result = mod_quiz_external::view_attempt($attempt->id, 1, $preflightdata);
1289          $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result);
1290          $this->assertTrue($result['status']);
1291  
1292          $events = $sink->get_events();
1293          $this->assertCount(2, $events);
1294  
1295          // Try to go to previous page.
1296          try {
1297              mod_quiz_external::view_attempt($attempt->id, 0);
1298              $this->fail('Exception expected due to try to see a previous page.');
1299          } catch (moodle_quiz_exception $e) {
1300              $this->assertEquals('Out of sequence access', $e->errorcode);
1301          }
1302  
1303      }
1304  
1305      /**
1306       * Test test_view_attempt_summary
1307       */
1308      public function test_view_attempt_summary() {
1309          global $DB;
1310  
1311          // Create a new quiz with two questions and one attempt started.
1312          list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false);
1313  
1314          // Test user with full capabilities.
1315          $this->setUser($this->student);
1316  
1317          // Trigger and capture the event.
1318          $sink = $this->redirectEvents();
1319  
1320          $result = mod_quiz_external::view_attempt_summary($attempt->id);
1321          $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result);
1322          $this->assertTrue($result['status']);
1323  
1324          $events = $sink->get_events();
1325          $this->assertCount(1, $events);
1326          $event = array_shift($events);
1327  
1328          // Checking that the event contains the expected values.
1329          $this->assertInstanceOf('\mod_quiz\event\attempt_summary_viewed', $event);
1330          $this->assertEquals($context, $event->get_context());
1331          $moodlequiz = new \moodle_url('/mod/quiz/summary.php', array('attempt' => $attempt->id));
1332          $this->assertEquals($moodlequiz, $event->get_url());
1333          $this->assertEventContextNotUsed($event);
1334          $this->assertNotEmpty($event->get_name());
1335  
1336          // Quiz requiring preflightdata.
1337          $DB->set_field('quiz', 'password', 'abcdef', array('id' => $quiz->id));
1338          $preflightdata = array(array("name" => "quizpassword", "value" => 'abcdef'));
1339  
1340          $result = mod_quiz_external::view_attempt_summary($attempt->id, $preflightdata);
1341          $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result);
1342          $this->assertTrue($result['status']);
1343  
1344      }
1345  
1346      /**
1347       * Test test_view_attempt_summary
1348       */
1349      public function test_view_attempt_review() {
1350          global $DB;
1351  
1352          // Create a new quiz with two questions and one attempt finished.
1353          list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true);
1354  
1355          // Test user with full capabilities.
1356          $this->setUser($this->student);
1357  
1358          // Trigger and capture the event.
1359          $sink = $this->redirectEvents();
1360  
1361          $result = mod_quiz_external::view_attempt_review($attempt->id, 0);
1362          $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_review_returns(), $result);
1363          $this->assertTrue($result['status']);
1364  
1365          $events = $sink->get_events();
1366          $this->assertCount(1, $events);
1367          $event = array_shift($events);
1368  
1369          // Checking that the event contains the expected values.
1370          $this->assertInstanceOf('\mod_quiz\event\attempt_reviewed', $event);
1371          $this->assertEquals($context, $event->get_context());
1372          $moodlequiz = new \moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->id));
1373          $this->assertEquals($moodlequiz, $event->get_url());
1374          $this->assertEventContextNotUsed($event);
1375          $this->assertNotEmpty($event->get_name());
1376  
1377      }
1378  
1379      /**
1380       * Test get_quiz_feedback_for_grade
1381       */
1382      public function test_get_quiz_feedback_for_grade() {
1383          global $DB;
1384  
1385          // Add feedback to the quiz.
1386          $feedback = new stdClass();
1387          $feedback->quizid = $this->quiz->id;
1388          $feedback->feedbacktext = 'Feedback text 1';
1389          $feedback->feedbacktextformat = 1;
1390          $feedback->mingrade = 49;
1391          $feedback->maxgrade = 100;
1392          $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1393  
1394          $feedback->feedbacktext = 'Feedback text 2';
1395          $feedback->feedbacktextformat = 1;
1396          $feedback->mingrade = 30;
1397          $feedback->maxgrade = 49;
1398          $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1399  
1400          $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 50);
1401          $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result);
1402          $this->assertEquals('Feedback text 1', $result['feedbacktext']);
1403          $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']);
1404  
1405          $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 30);
1406          $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result);
1407          $this->assertEquals('Feedback text 2', $result['feedbacktext']);
1408          $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']);
1409  
1410          $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 10);
1411          $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result);
1412          $this->assertEquals('', $result['feedbacktext']);
1413          $this->assertEquals(FORMAT_MOODLE, $result['feedbacktextformat']);
1414      }
1415  
1416      /**
1417       * Test get_quiz_access_information
1418       */
1419      public function test_get_quiz_access_information() {
1420          global $DB;
1421  
1422          // Create a new quiz.
1423          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
1424          $data = array('course' => $this->course->id);
1425          $quiz = $quizgenerator->create_instance($data);
1426  
1427          $this->setUser($this->student);
1428  
1429          // Default restrictions (none).
1430          $result = mod_quiz_external::get_quiz_access_information($quiz->id);
1431          $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result);
1432  
1433          $expected = array(
1434              'canattempt' => true,
1435              'canmanage' => false,
1436              'canpreview' => false,
1437              'canreviewmyattempts' => true,
1438              'canviewreports' => false,
1439              'accessrules' => [],
1440              // This rule is always used, even if the quiz has no open or close date.
1441              'activerulenames' => ['quizaccess_openclosedate'],
1442              'preventaccessreasons' => [],
1443              'warnings' => []
1444          );
1445  
1446          $this->assertEquals($expected, $result);
1447  
1448          // Now teacher, different privileges.
1449          $this->setUser($this->teacher);
1450          $result = mod_quiz_external::get_quiz_access_information($quiz->id);
1451          $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result);
1452  
1453          $expected['canmanage'] = true;
1454          $expected['canpreview'] = true;
1455          $expected['canviewreports'] = true;
1456          $expected['canattempt'] = false;
1457          $expected['canreviewmyattempts'] = false;
1458  
1459          $this->assertEquals($expected, $result);
1460  
1461          $this->setUser($this->student);
1462          // Now add some restrictions.
1463          $quiz->timeopen = time() + DAYSECS;
1464          $quiz->timeclose = time() + WEEKSECS;
1465          $quiz->password = '123456';
1466          $DB->update_record('quiz', $quiz);
1467  
1468          $result = mod_quiz_external::get_quiz_access_information($quiz->id);
1469          $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result);
1470  
1471          // Access limited by time and password.
1472          $this->assertCount(3, $result['accessrules']);
1473          // Two rule names, password and open/close date.
1474          $this->assertCount(2, $result['activerulenames']);
1475          $this->assertCount(1, $result['preventaccessreasons']);
1476  
1477      }
1478  
1479      /**
1480       * Test get_attempt_access_information
1481       */
1482      public function test_get_attempt_access_information() {
1483          global $DB;
1484  
1485          // Create a new quiz with attempts.
1486          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
1487          $data = array('course' => $this->course->id,
1488                        'sumgrades' => 2);
1489          $quiz = $quizgenerator->create_instance($data);
1490  
1491          // Create some questions.
1492          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
1493  
1494          $cat = $questiongenerator->create_question_category();
1495          $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
1496          quiz_add_quiz_question($question->id, $quiz);
1497  
1498          $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
1499          quiz_add_quiz_question($question->id, $quiz);
1500  
1501          // Add new question types in the category (for the random one).
1502          $question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id));
1503          $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id));
1504  
1505          $question = $questiongenerator->create_question('random', null, array('category' => $cat->id));
1506          quiz_add_quiz_question($question->id, $quiz);
1507  
1508          $quizobj = quiz::create($quiz->id, $this->student->id);
1509  
1510          // Set grade to pass.
1511          $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
1512                                          'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
1513          $item->gradepass = 80;
1514          $item->update();
1515  
1516          $this->setUser($this->student);
1517  
1518          // Default restrictions (none).
1519          $result = mod_quiz_external::get_attempt_access_information($quiz->id);
1520          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result);
1521  
1522          $expected = array(
1523              'isfinished' => false,
1524              'preventnewattemptreasons' => [],
1525              'warnings' => []
1526          );
1527  
1528          $this->assertEquals($expected, $result);
1529  
1530          // Limited attempts.
1531          $quiz->attempts = 1;
1532          $DB->update_record('quiz', $quiz);
1533  
1534          // Now, do one attempt.
1535          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1536          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1537  
1538          $timenow = time();
1539          $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
1540          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
1541          quiz_attempt_save_started($quizobj, $quba, $attempt);
1542  
1543          // Process some responses from the student.
1544          $attemptobj = quiz_attempt::create($attempt->id);
1545          $tosubmit = array(1 => array('answer' => '3.14'));
1546          $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
1547  
1548          // Finish the attempt.
1549          $attemptobj = quiz_attempt::create($attempt->id);
1550          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
1551          $attemptobj->process_finish($timenow, false);
1552  
1553          // Can we start a new attempt? We shall not!
1554          $result = mod_quiz_external::get_attempt_access_information($quiz->id, $attempt->id);
1555          $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result);
1556  
1557          // Now new attemps allowed.
1558          $this->assertCount(1, $result['preventnewattemptreasons']);
1559          $this->assertFalse($result['ispreflightcheckrequired']);
1560          $this->assertEquals(get_string('nomoreattempts', 'quiz'), $result['preventnewattemptreasons'][0]);
1561  
1562      }
1563  
1564      /**
1565       * Test get_quiz_required_qtypes
1566       */
1567      public function test_get_quiz_required_qtypes() {
1568          global $DB;
1569  
1570          // Create a new quiz.
1571          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
1572          $data = array('course' => $this->course->id);
1573          $quiz = $quizgenerator->create_instance($data);
1574  
1575          // Create some questions.
1576          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
1577  
1578          $cat = $questiongenerator->create_question_category();
1579          $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
1580          quiz_add_quiz_question($question->id, $quiz);
1581  
1582          $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
1583          quiz_add_quiz_question($question->id, $quiz);
1584  
1585          // Add new question types in the category (for the random one).
1586          $question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id));
1587          $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id));
1588  
1589          $question = $questiongenerator->create_question('random', null, array('category' => $cat->id));
1590          quiz_add_quiz_question($question->id, $quiz);
1591  
1592          $this->setUser($this->student);
1593  
1594          $result = mod_quiz_external::get_quiz_required_qtypes($quiz->id);
1595          $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_required_qtypes_returns(), $result);
1596  
1597          $expected = array(
1598              'questiontypes' => ['essay', 'numerical', 'random', 'shortanswer', 'truefalse'],
1599              'warnings' => []
1600          );
1601  
1602          $this->assertEquals($expected, $result);
1603  
1604      }
1605  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1