Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Quiz external API
  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  require_once($CFG->libdir . '/externallib.php');
  30  require_once($CFG->dirroot . '/mod/quiz/locallib.php');
  31  
  32  /**
  33   * Quiz external functions
  34   *
  35   * @package    mod_quiz
  36   * @category   external
  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 mod_quiz_external extends external_api {
  42  
  43      /**
  44       * Describes the parameters for get_quizzes_by_courses.
  45       *
  46       * @return external_function_parameters
  47       * @since Moodle 3.1
  48       */
  49      public static function get_quizzes_by_courses_parameters() {
  50          return new external_function_parameters (
  51              array(
  52                  'courseids' => new external_multiple_structure(
  53                      new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, array()
  54                  ),
  55              )
  56          );
  57      }
  58  
  59      /**
  60       * Returns a list of quizzes in a provided list of courses,
  61       * if no list is provided all quizzes that the user can view will be returned.
  62       *
  63       * @param array $courseids Array of course ids
  64       * @return array of quizzes details
  65       * @since Moodle 3.1
  66       */
  67      public static function get_quizzes_by_courses($courseids = array()) {
  68          global $USER;
  69  
  70          $warnings = array();
  71          $returnedquizzes = array();
  72  
  73          $params = array(
  74              'courseids' => $courseids,
  75          );
  76          $params = self::validate_parameters(self::get_quizzes_by_courses_parameters(), $params);
  77  
  78          $mycourses = array();
  79          if (empty($params['courseids'])) {
  80              $mycourses = enrol_get_my_courses();
  81              $params['courseids'] = array_keys($mycourses);
  82          }
  83  
  84          // Ensure there are courseids to loop through.
  85          if (!empty($params['courseids'])) {
  86  
  87              list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses);
  88  
  89              // Get the quizzes in this course, this function checks users visibility permissions.
  90              // We can avoid then additional validate_context calls.
  91              $quizzes = get_all_instances_in_courses("quiz", $courses);
  92              foreach ($quizzes as $quiz) {
  93                  $context = context_module::instance($quiz->coursemodule);
  94  
  95                  // Update quiz with override information.
  96                  $quiz = quiz_update_effective_access($quiz, $USER->id);
  97  
  98                  // Entry to return.
  99                  $quizdetails = array();
 100                  // First, we return information that any user can see in the web interface.
 101                  $quizdetails['id'] = $quiz->id;
 102                  $quizdetails['coursemodule']      = $quiz->coursemodule;
 103                  $quizdetails['course']            = $quiz->course;
 104                  $quizdetails['name']              = external_format_string($quiz->name, $context->id);
 105  
 106                  if (has_capability('mod/quiz:view', $context)) {
 107                      // Format intro.
 108                      $options = array('noclean' => true);
 109                      list($quizdetails['intro'], $quizdetails['introformat']) =
 110                          external_format_text($quiz->intro, $quiz->introformat, $context->id, 'mod_quiz', 'intro', null, $options);
 111  
 112                      $quizdetails['introfiles'] = external_util::get_area_files($context->id, 'mod_quiz', 'intro', false, false);
 113                      $viewablefields = array('timeopen', 'timeclose', 'grademethod', 'section', 'visible', 'groupmode',
 114                                              'groupingid', 'attempts', 'timelimit', 'grademethod', 'decimalpoints',
 115                                              'questiondecimalpoints', 'sumgrades', 'grade', 'preferredbehaviour');
 116                      // Some times this function returns just empty.
 117                      $hasfeedback = quiz_has_feedback($quiz);
 118                      $quizdetails['hasfeedback'] = (!empty($hasfeedback)) ? 1 : 0;
 119  
 120                      $timenow = time();
 121                      $quizobj = quiz::create($quiz->id, $USER->id);
 122                      $accessmanager = new quiz_access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits',
 123                                                                  $context, null, false));
 124  
 125                      // Fields the user could see if have access to the quiz.
 126                      if (!$accessmanager->prevent_access()) {
 127                          $quizdetails['hasquestions'] = (int) $quizobj->has_questions();
 128                          $quizdetails['autosaveperiod'] = get_config('quiz', 'autosaveperiod');
 129  
 130                          $additionalfields = array('attemptonlast', 'reviewattempt', 'reviewcorrectness', 'reviewmarks',
 131                                                      'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer',
 132                                                      'reviewoverallfeedback', 'questionsperpage', 'navmethod',
 133                                                      'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks',
 134                                                      'completionattemptsexhausted', 'completionpass', 'overduehandling',
 135                                                      'graceperiod', 'canredoquestions', 'allowofflineattempts');
 136                          $viewablefields = array_merge($viewablefields, $additionalfields);
 137                      }
 138  
 139                      // Fields only for managers.
 140                      if (has_capability('moodle/course:manageactivities', $context)) {
 141                          $additionalfields = array('shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet');
 142                          $viewablefields = array_merge($viewablefields, $additionalfields);
 143                      }
 144  
 145                      foreach ($viewablefields as $field) {
 146                          $quizdetails[$field] = $quiz->{$field};
 147                      }
 148                  }
 149                  $returnedquizzes[] = $quizdetails;
 150              }
 151          }
 152          $result = array();
 153          $result['quizzes'] = $returnedquizzes;
 154          $result['warnings'] = $warnings;
 155          return $result;
 156      }
 157  
 158      /**
 159       * Describes the get_quizzes_by_courses return value.
 160       *
 161       * @return external_single_structure
 162       * @since Moodle 3.1
 163       */
 164      public static function get_quizzes_by_courses_returns() {
 165          return new external_single_structure(
 166              array(
 167                  'quizzes' => new external_multiple_structure(
 168                      new external_single_structure(
 169                          array(
 170                              'id' => new external_value(PARAM_INT, 'Standard Moodle primary key.'),
 171                              'course' => new external_value(PARAM_INT, 'Foreign key reference to the course this quiz is part of.'),
 172                              'coursemodule' => new external_value(PARAM_INT, 'Course module id.'),
 173                              'name' => new external_value(PARAM_RAW, 'Quiz name.'),
 174                              'intro' => new external_value(PARAM_RAW, 'Quiz introduction text.', VALUE_OPTIONAL),
 175                              'introformat' => new external_format_value('intro', VALUE_OPTIONAL),
 176                              'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
 177                              'timeopen' => new external_value(PARAM_INT, 'The time when this quiz opens. (0 = no restriction.)',
 178                                                                  VALUE_OPTIONAL),
 179                              'timeclose' => new external_value(PARAM_INT, 'The time when this quiz closes. (0 = no restriction.)',
 180                                                                  VALUE_OPTIONAL),
 181                              'timelimit' => new external_value(PARAM_INT, 'The time limit for quiz attempts, in seconds.',
 182                                                                  VALUE_OPTIONAL),
 183                              'overduehandling' => new external_value(PARAM_ALPHA, 'The method used to handle overdue attempts.
 184                                                                      \'autosubmit\', \'graceperiod\' or \'autoabandon\'.',
 185                                                                      VALUE_OPTIONAL),
 186                              'graceperiod' => new external_value(PARAM_INT, 'The amount of time (in seconds) after the time limit
 187                                                                  runs out during which attempts can still be submitted,
 188                                                                  if overduehandling is set to allow it.', VALUE_OPTIONAL),
 189                              'preferredbehaviour' => new external_value(PARAM_ALPHANUMEXT, 'The behaviour to ask questions to use.',
 190                                                                          VALUE_OPTIONAL),
 191                              'canredoquestions' => new external_value(PARAM_INT, 'Allows students to redo any completed question
 192                                                                          within a quiz attempt.', VALUE_OPTIONAL),
 193                              'attempts' => new external_value(PARAM_INT, 'The maximum number of attempts a student is allowed.',
 194                                                                  VALUE_OPTIONAL),
 195                              'attemptonlast' => new external_value(PARAM_INT, 'Whether subsequent attempts start from the answer
 196                                                                      to the previous attempt (1) or start blank (0).',
 197                                                                      VALUE_OPTIONAL),
 198                              'grademethod' => new external_value(PARAM_INT, 'One of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE,
 199                                                                      QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.', VALUE_OPTIONAL),
 200                              'decimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when displaying
 201                                                                      grades.', VALUE_OPTIONAL),
 202                              'questiondecimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when
 203                                                                              displaying question grades.
 204                                                                              (-1 means use decimalpoints.)', VALUE_OPTIONAL),
 205                              'reviewattempt' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
 206                                                                      attempts at various times. This is a bit field, decoded by the
 207                                                                      mod_quiz_display_options class. It is formed by ORing together
 208                                                                      the constants defined there.', VALUE_OPTIONAL),
 209                              'reviewcorrectness' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
 210                                                                          attempts at various times.
 211                                                                          A bit field, like reviewattempt.', VALUE_OPTIONAL),
 212                              'reviewmarks' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz attempts
 213                                                                  at various times. A bit field, like reviewattempt.',
 214                                                                  VALUE_OPTIONAL),
 215                              'reviewspecificfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
 216                                                                              quiz attempts at various times. A bit field, like
 217                                                                              reviewattempt.', VALUE_OPTIONAL),
 218                              'reviewgeneralfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
 219                                                                              quiz attempts at various times. A bit field, like
 220                                                                              reviewattempt.', VALUE_OPTIONAL),
 221                              'reviewrightanswer' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
 222                                                                          attempts at various times. A bit field, like
 223                                                                          reviewattempt.', VALUE_OPTIONAL),
 224                              'reviewoverallfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
 225                                                                              attempts at various times. A bit field, like
 226                                                                              reviewattempt.', VALUE_OPTIONAL),
 227                              'questionsperpage' => new external_value(PARAM_INT, 'How often to insert a page break when editing
 228                                                                          the quiz, or when shuffling the question order.',
 229                                                                          VALUE_OPTIONAL),
 230                              'navmethod' => new external_value(PARAM_ALPHA, 'Any constraints on how the user is allowed to navigate
 231                                                                  around the quiz. Currently recognised values are
 232                                                                  \'free\' and \'seq\'.', VALUE_OPTIONAL),
 233                              'shuffleanswers' => new external_value(PARAM_INT, 'Whether the parts of the question should be shuffled,
 234                                                                      in those question types that support it.', VALUE_OPTIONAL),
 235                              'sumgrades' => new external_value(PARAM_FLOAT, 'The total of all the question instance maxmarks.',
 236                                                                  VALUE_OPTIONAL),
 237                              'grade' => new external_value(PARAM_FLOAT, 'The total that the quiz overall grade is scaled to be
 238                                                              out of.', VALUE_OPTIONAL),
 239                              'timecreated' => new external_value(PARAM_INT, 'The time when the quiz was added to the course.',
 240                                                                  VALUE_OPTIONAL),
 241                              'timemodified' => new external_value(PARAM_INT, 'Last modified time.',
 242                                                                      VALUE_OPTIONAL),
 243                              'password' => new external_value(PARAM_RAW, 'A password that the student must enter before starting or
 244                                                                  continuing a quiz attempt.', VALUE_OPTIONAL),
 245                              'subnet' => new external_value(PARAM_RAW, 'Used to restrict the IP addresses from which this quiz can
 246                                                              be attempted. The format is as requried by the address_in_subnet
 247                                                              function.', VALUE_OPTIONAL),
 248                              'browsersecurity' => new external_value(PARAM_ALPHANUMEXT, 'Restriciton on the browser the student must
 249                                                                      use. E.g. \'securewindow\'.', VALUE_OPTIONAL),
 250                              'delay1' => new external_value(PARAM_INT, 'Delay that must be left between the first and second attempt,
 251                                                              in seconds.', VALUE_OPTIONAL),
 252                              'delay2' => new external_value(PARAM_INT, 'Delay that must be left between the second and subsequent
 253                                                              attempt, in seconds.', VALUE_OPTIONAL),
 254                              'showuserpicture' => new external_value(PARAM_INT, 'Option to show the user\'s picture during the
 255                                                                      attempt and on the review page.', VALUE_OPTIONAL),
 256                              'showblocks' => new external_value(PARAM_INT, 'Whether blocks should be shown on the attempt.php and
 257                                                                  review.php pages.', VALUE_OPTIONAL),
 258                              'completionattemptsexhausted' => new external_value(PARAM_INT, 'Mark quiz complete when the student has
 259                                                                                  exhausted the maximum number of attempts',
 260                                                                                  VALUE_OPTIONAL),
 261                              'completionpass' => new external_value(PARAM_INT, 'Whether to require passing grade', VALUE_OPTIONAL),
 262                              'allowofflineattempts' => new external_value(PARAM_INT, 'Whether to allow the quiz to be attempted
 263                                                                              offline in the mobile app', VALUE_OPTIONAL),
 264                              'autosaveperiod' => new external_value(PARAM_INT, 'Auto-save delay', VALUE_OPTIONAL),
 265                              'hasfeedback' => new external_value(PARAM_INT, 'Whether the quiz has any non-blank feedback text',
 266                                                                  VALUE_OPTIONAL),
 267                              'hasquestions' => new external_value(PARAM_INT, 'Whether the quiz has questions', VALUE_OPTIONAL),
 268                              'section' => new external_value(PARAM_INT, 'Course section id', VALUE_OPTIONAL),
 269                              'visible' => new external_value(PARAM_INT, 'Module visibility', VALUE_OPTIONAL),
 270                              'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
 271                              'groupingid' => new external_value(PARAM_INT, 'Grouping id', VALUE_OPTIONAL),
 272                          )
 273                      )
 274                  ),
 275                  'warnings' => new external_warnings(),
 276              )
 277          );
 278      }
 279  
 280  
 281      /**
 282       * Utility function for validating a quiz.
 283       *
 284       * @param int $quizid quiz instance id
 285       * @return array array containing the quiz, course, context and course module objects
 286       * @since  Moodle 3.1
 287       */
 288      protected static function validate_quiz($quizid) {
 289          global $DB;
 290  
 291          // Request and permission validation.
 292          $quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST);
 293          list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
 294  
 295          $context = context_module::instance($cm->id);
 296          self::validate_context($context);
 297  
 298          return array($quiz, $course, $cm, $context);
 299      }
 300  
 301      /**
 302       * Describes the parameters for view_quiz.
 303       *
 304       * @return external_function_parameters
 305       * @since Moodle 3.1
 306       */
 307      public static function view_quiz_parameters() {
 308          return new external_function_parameters (
 309              array(
 310                  'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
 311              )
 312          );
 313      }
 314  
 315      /**
 316       * Trigger the course module viewed event and update the module completion status.
 317       *
 318       * @param int $quizid quiz instance id
 319       * @return array of warnings and status result
 320       * @since Moodle 3.1
 321       * @throws moodle_exception
 322       */
 323      public static function view_quiz($quizid) {
 324          global $DB;
 325  
 326          $params = self::validate_parameters(self::view_quiz_parameters(), array('quizid' => $quizid));
 327          $warnings = array();
 328  
 329          list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
 330  
 331          // Trigger course_module_viewed event and completion.
 332          quiz_view($quiz, $course, $cm, $context);
 333  
 334          $result = array();
 335          $result['status'] = true;
 336          $result['warnings'] = $warnings;
 337          return $result;
 338      }
 339  
 340      /**
 341       * Describes the view_quiz return value.
 342       *
 343       * @return external_single_structure
 344       * @since Moodle 3.1
 345       */
 346      public static function view_quiz_returns() {
 347          return new external_single_structure(
 348              array(
 349                  'status' => new external_value(PARAM_BOOL, 'status: true if success'),
 350                  'warnings' => new external_warnings(),
 351              )
 352          );
 353      }
 354  
 355      /**
 356       * Describes the parameters for get_user_attempts.
 357       *
 358       * @return external_function_parameters
 359       * @since Moodle 3.1
 360       */
 361      public static function get_user_attempts_parameters() {
 362          return new external_function_parameters (
 363              array(
 364                  'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
 365                  'userid' => new external_value(PARAM_INT, 'user id, empty for current user', VALUE_DEFAULT, 0),
 366                  'status' => new external_value(PARAM_ALPHA, 'quiz status: all, finished or unfinished', VALUE_DEFAULT, 'finished'),
 367                  'includepreviews' => new external_value(PARAM_BOOL, 'whether to include previews or not', VALUE_DEFAULT, false),
 368  
 369              )
 370          );
 371      }
 372  
 373      /**
 374       * Return a list of attempts for the given quiz and user.
 375       *
 376       * @param int $quizid quiz instance id
 377       * @param int $userid user id
 378       * @param string $status quiz status: all, finished or unfinished
 379       * @param bool $includepreviews whether to include previews or not
 380       * @return array of warnings and the list of attempts
 381       * @since Moodle 3.1
 382       * @throws invalid_parameter_exception
 383       */
 384      public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) {
 385          global $DB, $USER;
 386  
 387          $warnings = array();
 388  
 389          $params = array(
 390              'quizid' => $quizid,
 391              'userid' => $userid,
 392              'status' => $status,
 393              'includepreviews' => $includepreviews,
 394          );
 395          $params = self::validate_parameters(self::get_user_attempts_parameters(), $params);
 396  
 397          list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
 398  
 399          if (!in_array($params['status'], array('all', 'finished', 'unfinished'))) {
 400              throw new invalid_parameter_exception('Invalid status value');
 401          }
 402  
 403          // Default value for userid.
 404          if (empty($params['userid'])) {
 405              $params['userid'] = $USER->id;
 406          }
 407  
 408          $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
 409          core_user::require_active_user($user);
 410  
 411          // Extra checks so only users with permissions can view other users attempts.
 412          if ($USER->id != $user->id) {
 413              require_capability('mod/quiz:viewreports', $context);
 414          }
 415  
 416          // Update quiz with override information.
 417          $quiz = quiz_update_effective_access($quiz, $params['userid']);
 418          $attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
 419          $attemptresponse = [];
 420          foreach ($attempts as $attempt) {
 421              $reviewoptions = quiz_get_review_options($quiz, $attempt, $context);
 422              if (!has_capability('mod/quiz:viewreports', $context) &&
 423                      ($reviewoptions->marks < question_display_options::MARK_AND_MAX || $attempt->state != quiz_attempt::FINISHED)) {
 424                  // Blank the mark if the teacher does not allow it.
 425                  $attempt->sumgrades = null;
 426              }
 427              $attemptresponse[] = $attempt;
 428          }
 429          $result = array();
 430          $result['attempts'] = $attemptresponse;
 431          $result['warnings'] = $warnings;
 432          return $result;
 433      }
 434  
 435      /**
 436       * Describes a single attempt structure.
 437       *
 438       * @return external_single_structure the attempt structure
 439       */
 440      private static function attempt_structure() {
 441          return new external_single_structure(
 442              array(
 443                  'id' => new external_value(PARAM_INT, 'Attempt id.', VALUE_OPTIONAL),
 444                  'quiz' => new external_value(PARAM_INT, 'Foreign key reference to the quiz that was attempted.',
 445                                                  VALUE_OPTIONAL),
 446                  'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.',
 447                                                  VALUE_OPTIONAL),
 448                  'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.',
 449                                                  VALUE_OPTIONAL),
 450                  'uniqueid' => new external_value(PARAM_INT, 'Foreign key reference to the question_usage that holds the
 451                                                      details of the the question_attempts that make up this quiz
 452                                                      attempt.', VALUE_OPTIONAL),
 453                  'layout' => new external_value(PARAM_RAW, 'Attempt layout.', VALUE_OPTIONAL),
 454                  'currentpage' => new external_value(PARAM_INT, 'Attempt current page.', VALUE_OPTIONAL),
 455                  'preview' => new external_value(PARAM_INT, 'Whether is a preview attempt or not.', VALUE_OPTIONAL),
 456                  'state' => new external_value(PARAM_ALPHA, 'The current state of the attempts. \'inprogress\',
 457                                                  \'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL),
 458                  'timestart' => new external_value(PARAM_INT, 'Time when the attempt was started.', VALUE_OPTIONAL),
 459                  'timefinish' => new external_value(PARAM_INT, 'Time when the attempt was submitted.
 460                                                      0 if the attempt has not been submitted yet.', VALUE_OPTIONAL),
 461                  'timemodified' => new external_value(PARAM_INT, 'Last modified time.', VALUE_OPTIONAL),
 462                  'timemodifiedoffline' => new external_value(PARAM_INT, 'Last modified time via webservices.', VALUE_OPTIONAL),
 463                  'timecheckstate' => new external_value(PARAM_INT, 'Next time quiz cron should check attempt for
 464                                                          state changes.  NULL means never check.', VALUE_OPTIONAL),
 465                  'sumgrades' => new external_value(PARAM_FLOAT, 'Total marks for this attempt.', VALUE_OPTIONAL),
 466              )
 467          );
 468      }
 469  
 470      /**
 471       * Describes the get_user_attempts return value.
 472       *
 473       * @return external_single_structure
 474       * @since Moodle 3.1
 475       */
 476      public static function get_user_attempts_returns() {
 477          return new external_single_structure(
 478              array(
 479                  'attempts' => new external_multiple_structure(self::attempt_structure()),
 480                  'warnings' => new external_warnings(),
 481              )
 482          );
 483      }
 484  
 485      /**
 486       * Describes the parameters for get_user_best_grade.
 487       *
 488       * @return external_function_parameters
 489       * @since Moodle 3.1
 490       */
 491      public static function get_user_best_grade_parameters() {
 492          return new external_function_parameters (
 493              array(
 494                  'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
 495                  'userid' => new external_value(PARAM_INT, 'user id', VALUE_DEFAULT, 0),
 496              )
 497          );
 498      }
 499  
 500      /**
 501       * Get the best current grade for the given user on a quiz.
 502       *
 503       * @param int $quizid quiz instance id
 504       * @param int $userid user id
 505       * @return array of warnings and the grade information
 506       * @since Moodle 3.1
 507       */
 508      public static function get_user_best_grade($quizid, $userid = 0) {
 509          global $DB, $USER, $CFG;
 510          require_once($CFG->libdir . '/gradelib.php');
 511  
 512          $warnings = array();
 513  
 514          $params = array(
 515              'quizid' => $quizid,
 516              'userid' => $userid,
 517          );
 518          $params = self::validate_parameters(self::get_user_best_grade_parameters(), $params);
 519  
 520          list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
 521  
 522          // Default value for userid.
 523          if (empty($params['userid'])) {
 524              $params['userid'] = $USER->id;
 525          }
 526  
 527          $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
 528          core_user::require_active_user($user);
 529  
 530          // Extra checks so only users with permissions can view other users attempts.
 531          if ($USER->id != $user->id) {
 532              require_capability('mod/quiz:viewreports', $context);
 533          }
 534  
 535          $result = array();
 536  
 537          // This code was mostly copied from mod/quiz/view.php. We need to make the web service logic consistent.
 538          // Get this user's attempts.
 539          $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all');
 540          $canviewgrade = false;
 541          if ($attempts) {
 542              if ($USER->id != $user->id) {
 543                  // No need to check the permission here. We did it at by require_capability('mod/quiz:viewreports', $context).
 544                  $canviewgrade = true;
 545              } else {
 546                  // Work out which columns we need, taking account what data is available in each attempt.
 547                  [$notused, $alloptions] = quiz_get_combined_reviewoptions($quiz, $attempts);
 548                  $canviewgrade = $alloptions->marks >= question_display_options::MARK_AND_MAX;
 549              }
 550          }
 551  
 552          $grade = $canviewgrade ? quiz_get_best_grade($quiz, $user->id) : null;
 553  
 554          if ($grade === null) {
 555              $result['hasgrade'] = false;
 556          } else {
 557              $result['hasgrade'] = true;
 558              $result['grade'] = $grade;
 559          }
 560  
 561          // Inform user of the grade to pass if non-zero.
 562          $gradinginfo = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
 563          if (!empty($gradinginfo->items)) {
 564              $item = $gradinginfo->items[0];
 565  
 566              if ($item && grade_floats_different($item->gradepass, 0)) {
 567                  $result['gradetopass'] = $item->gradepass;
 568              }
 569          }
 570  
 571          $result['warnings'] = $warnings;
 572          return $result;
 573      }
 574  
 575      /**
 576       * Describes the get_user_best_grade return value.
 577       *
 578       * @return external_single_structure
 579       * @since Moodle 3.1
 580       */
 581      public static function get_user_best_grade_returns() {
 582          return new external_single_structure(
 583              array(
 584                  'hasgrade' => new external_value(PARAM_BOOL, 'Whether the user has a grade on the given quiz.'),
 585                  'grade' => new external_value(PARAM_FLOAT, 'The grade (only if the user has a grade).', VALUE_OPTIONAL),
 586                  'gradetopass' => new external_value(PARAM_FLOAT, 'The grade to pass the quiz (only if set).', VALUE_OPTIONAL),
 587                  'warnings' => new external_warnings(),
 588              )
 589          );
 590      }
 591  
 592      /**
 593       * Describes the parameters for get_combined_review_options.
 594       *
 595       * @return external_function_parameters
 596       * @since Moodle 3.1
 597       */
 598      public static function get_combined_review_options_parameters() {
 599          return new external_function_parameters (
 600              array(
 601                  'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
 602                  'userid' => new external_value(PARAM_INT, 'user id (empty for current user)', VALUE_DEFAULT, 0),
 603  
 604              )
 605          );
 606      }
 607  
 608      /**
 609       * Combines the review options from a number of different quiz attempts.
 610       *
 611       * @param int $quizid quiz instance id
 612       * @param int $userid user id (empty for current user)
 613       * @return array of warnings and the review options
 614       * @since Moodle 3.1
 615       */
 616      public static function get_combined_review_options($quizid, $userid = 0) {
 617          global $DB, $USER;
 618  
 619          $warnings = array();
 620  
 621          $params = array(
 622              'quizid' => $quizid,
 623              'userid' => $userid,
 624          );
 625          $params = self::validate_parameters(self::get_combined_review_options_parameters(), $params);
 626  
 627          list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
 628  
 629          // Default value for userid.
 630          if (empty($params['userid'])) {
 631              $params['userid'] = $USER->id;
 632          }
 633  
 634          $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
 635          core_user::require_active_user($user);
 636  
 637          // Extra checks so only users with permissions can view other users attempts.
 638          if ($USER->id != $user->id) {
 639              require_capability('mod/quiz:viewreports', $context);
 640          }
 641  
 642          $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all', true);
 643  
 644          $result = array();
 645          $result['someoptions'] = [];
 646          $result['alloptions'] = [];
 647  
 648          list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts);
 649  
 650          foreach (array('someoptions', 'alloptions') as $typeofoption) {
 651              foreach ($$typeofoption as $key => $value) {
 652                  $result[$typeofoption][] = array(
 653                      "name" => $key,
 654                      "value" => (!empty($value)) ? $value : 0
 655                  );
 656              }
 657          }
 658  
 659          $result['warnings'] = $warnings;
 660          return $result;
 661      }
 662  
 663      /**
 664       * Describes the get_combined_review_options return value.
 665       *
 666       * @return external_single_structure
 667       * @since Moodle 3.1
 668       */
 669      public static function get_combined_review_options_returns() {
 670          return new external_single_structure(
 671              array(
 672                  'someoptions' => new external_multiple_structure(
 673                      new external_single_structure(
 674                          array(
 675                              'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
 676                              'value' => new external_value(PARAM_INT, 'option value'),
 677                          )
 678                      )
 679                  ),
 680                  'alloptions' => new external_multiple_structure(
 681                      new external_single_structure(
 682                          array(
 683                              'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
 684                              'value' => new external_value(PARAM_INT, 'option value'),
 685                          )
 686                      )
 687                  ),
 688                  'warnings' => new external_warnings(),
 689              )
 690          );
 691      }
 692  
 693      /**
 694       * Describes the parameters for start_attempt.
 695       *
 696       * @return external_function_parameters
 697       * @since Moodle 3.1
 698       */
 699      public static function start_attempt_parameters() {
 700          return new external_function_parameters (
 701              array(
 702                  'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
 703                  'preflightdata' => new external_multiple_structure(
 704                      new external_single_structure(
 705                          array(
 706                              'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
 707                              'value' => new external_value(PARAM_RAW, 'data value'),
 708                          )
 709                      ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
 710                  ),
 711                  'forcenew' => new external_value(PARAM_BOOL, 'Whether to force a new attempt or not.', VALUE_DEFAULT, false),
 712  
 713              )
 714          );
 715      }
 716  
 717      /**
 718       * Starts a new attempt at a quiz.
 719       *
 720       * @param int $quizid quiz instance id
 721       * @param array $preflightdata preflight required data (like passwords)
 722       * @param bool $forcenew Whether to force a new attempt or not.
 723       * @return array of warnings and the attempt basic data
 724       * @since Moodle 3.1
 725       * @throws moodle_quiz_exception
 726       */
 727      public static function start_attempt($quizid, $preflightdata = array(), $forcenew = false) {
 728          global $DB, $USER;
 729  
 730          $warnings = array();
 731          $attempt = array();
 732  
 733          $params = array(
 734              'quizid' => $quizid,
 735              'preflightdata' => $preflightdata,
 736              'forcenew' => $forcenew,
 737          );
 738          $params = self::validate_parameters(self::start_attempt_parameters(), $params);
 739          $forcenew = $params['forcenew'];
 740  
 741          list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
 742  
 743          $quizobj = quiz::create($cm->instance, $USER->id);
 744  
 745          // Check questions.
 746          if (!$quizobj->has_questions()) {
 747              throw new moodle_quiz_exception($quizobj, 'noquestionsfound');
 748          }
 749  
 750          // Create an object to manage all the other (non-roles) access rules.
 751          $timenow = time();
 752          $accessmanager = $quizobj->get_access_manager($timenow);
 753  
 754          // Validate permissions for creating a new attempt and start a new preview attempt if required.
 755          list($currentattemptid, $attemptnumber, $lastattempt, $messages, $page) =
 756              quiz_validate_new_attempt($quizobj, $accessmanager, $forcenew, -1, false);
 757  
 758          // Check access.
 759          if (!$quizobj->is_preview_user() && $messages) {
 760              // Create warnings with the exact messages.
 761              foreach ($messages as $message) {
 762                  $warnings[] = array(
 763                      'item' => 'quiz',
 764                      'itemid' => $quiz->id,
 765                      'warningcode' => '1',
 766                      'message' => clean_text($message, PARAM_TEXT)
 767                  );
 768              }
 769          } else {
 770              if ($accessmanager->is_preflight_check_required($currentattemptid)) {
 771                  // Need to do some checks before allowing the user to continue.
 772  
 773                  $provideddata = array();
 774                  foreach ($params['preflightdata'] as $data) {
 775                      $provideddata[$data['name']] = $data['value'];
 776                  }
 777  
 778                  $errors = $accessmanager->validate_preflight_check($provideddata, [], $currentattemptid);
 779  
 780                  if (!empty($errors)) {
 781                      throw new moodle_quiz_exception($quizobj, array_shift($errors));
 782                  }
 783  
 784                  // Pre-flight check passed.
 785                  $accessmanager->notify_preflight_check_passed($currentattemptid);
 786              }
 787  
 788              if ($currentattemptid) {
 789                  if ($lastattempt->state == quiz_attempt::OVERDUE) {
 790                      throw new moodle_quiz_exception($quizobj, 'stateoverdue');
 791                  } else {
 792                      throw new moodle_quiz_exception($quizobj, 'attemptstillinprogress');
 793                  }
 794              }
 795              $offlineattempt = WS_SERVER ? true : false;
 796              $attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt, $offlineattempt);
 797          }
 798  
 799          $result = array();
 800          $result['attempt'] = $attempt;
 801          $result['warnings'] = $warnings;
 802          return $result;
 803      }
 804  
 805      /**
 806       * Describes the start_attempt return value.
 807       *
 808       * @return external_single_structure
 809       * @since Moodle 3.1
 810       */
 811      public static function start_attempt_returns() {
 812          return new external_single_structure(
 813              array(
 814                  'attempt' => self::attempt_structure(),
 815                  'warnings' => new external_warnings(),
 816              )
 817          );
 818      }
 819  
 820      /**
 821       * Utility function for validating a given attempt
 822       *
 823       * @param  array $params array of parameters including the attemptid and preflight data
 824       * @param  bool $checkaccessrules whether to check the quiz access rules or not
 825       * @param  bool $failifoverdue whether to return error if the attempt is overdue
 826       * @return  array containing the attempt object and access messages
 827       * @throws moodle_quiz_exception
 828       * @since  Moodle 3.1
 829       */
 830      protected static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
 831          global $USER;
 832  
 833          $attemptobj = quiz_attempt::create($params['attemptid']);
 834  
 835          $context = context_module::instance($attemptobj->get_cm()->id);
 836          self::validate_context($context);
 837  
 838          // Check that this attempt belongs to this user.
 839          if ($attemptobj->get_userid() != $USER->id) {
 840              throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
 841          }
 842  
 843          // General capabilities check.
 844          $ispreviewuser = $attemptobj->is_preview_user();
 845          if (!$ispreviewuser) {
 846              $attemptobj->require_capability('mod/quiz:attempt');
 847          }
 848  
 849          // Check the access rules.
 850          $accessmanager = $attemptobj->get_access_manager(time());
 851          $messages = array();
 852          if ($checkaccessrules) {
 853              // If the attempt is now overdue, or abandoned, deal with that.
 854              $attemptobj->handle_if_time_expired(time(), true);
 855  
 856              $messages = $accessmanager->prevent_access();
 857              if (!$ispreviewuser && $messages) {
 858                  throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attempterror');
 859              }
 860          }
 861  
 862          // Attempt closed?.
 863          if ($attemptobj->is_finished()) {
 864              throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptalreadyclosed');
 865          } else if ($failifoverdue && $attemptobj->get_state() == quiz_attempt::OVERDUE) {
 866              throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'stateoverdue');
 867          }
 868  
 869          // User submitted data (like the quiz password).
 870          if ($accessmanager->is_preflight_check_required($attemptobj->get_attemptid())) {
 871              $provideddata = array();
 872              foreach ($params['preflightdata'] as $data) {
 873                  $provideddata[$data['name']] = $data['value'];
 874              }
 875  
 876              $errors = $accessmanager->validate_preflight_check($provideddata, [], $params['attemptid']);
 877              if (!empty($errors)) {
 878                  throw new moodle_quiz_exception($attemptobj->get_quizobj(), array_shift($errors));
 879              }
 880              // Pre-flight check passed.
 881              $accessmanager->notify_preflight_check_passed($params['attemptid']);
 882          }
 883  
 884          if (isset($params['page'])) {
 885              // Check if the page is out of range.
 886              if ($params['page'] != $attemptobj->force_page_number_into_range($params['page'])) {
 887                  throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Invalid page number');
 888              }
 889  
 890              // Prevent out of sequence access.
 891              if (!$attemptobj->check_page_access($params['page'])) {
 892                  throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
 893              }
 894  
 895              // Check slots.
 896              $slots = $attemptobj->get_slots($params['page']);
 897  
 898              if (empty($slots)) {
 899                  throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound');
 900              }
 901          }
 902  
 903          return array($attemptobj, $messages);
 904      }
 905  
 906      /**
 907       * Describes a single question structure.
 908       *
 909       * @return external_single_structure the question data. Some fields may not be returned depending on the quiz display settings.
 910       * @since  Moodle 3.1
 911       * @since Moodle 3.2 blockedbyprevious parameter added.
 912       */
 913      private static function question_structure() {
 914          return new external_single_structure(
 915              array(
 916                  'slot' => new external_value(PARAM_INT, 'slot number'),
 917                  'type' => new external_value(PARAM_ALPHANUMEXT, 'question type, i.e: multichoice'),
 918                  'page' => new external_value(PARAM_INT, 'page of the quiz this question appears on'),
 919                  'html' => new external_value(PARAM_RAW, 'the question rendered'),
 920                  'responsefileareas' => new external_multiple_structure(
 921                      new external_single_structure(
 922                          array(
 923                              'area' => new external_value(PARAM_NOTAGS, 'File area name'),
 924                              'files' => new external_files('Response files for the question', VALUE_OPTIONAL),
 925                          )
 926                      ), 'Response file areas including files', VALUE_OPTIONAL
 927                  ),
 928                  'sequencecheck' => new external_value(PARAM_INT, 'the number of real steps in this attempt', VALUE_OPTIONAL),
 929                  'lastactiontime' => new external_value(PARAM_INT, 'the timestamp of the most recent step in this question attempt',
 930                                                          VALUE_OPTIONAL),
 931                  'hasautosavedstep' => new external_value(PARAM_BOOL, 'whether this question attempt has autosaved data',
 932                                                              VALUE_OPTIONAL),
 933                  'flagged' => new external_value(PARAM_BOOL, 'whether the question is flagged or not'),
 934                  'number' => new external_value(PARAM_INT, 'question ordering number in the quiz', VALUE_OPTIONAL),
 935                  'state' => new external_value(PARAM_ALPHA, 'the state where the question is in.
 936                      It will not be returned if the user cannot see it due to the quiz display correctness settings.',
 937                      VALUE_OPTIONAL),
 938                  'status' => new external_value(PARAM_RAW, 'current formatted state of the question', VALUE_OPTIONAL),
 939                  'blockedbyprevious' => new external_value(PARAM_BOOL, 'whether the question is blocked by the previous question',
 940                      VALUE_OPTIONAL),
 941                  'mark' => new external_value(PARAM_RAW, 'the mark awarded.
 942                      It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL),
 943                  'maxmark' => new external_value(PARAM_FLOAT, 'the maximum mark possible for this question attempt.
 944                      It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL),
 945                  'settings' => new external_value(PARAM_RAW, 'Question settings (JSON encoded).', VALUE_OPTIONAL),
 946              ),
 947              'The question data. Some fields may not be returned depending on the quiz display settings.'
 948          );
 949      }
 950  
 951      /**
 952       * Return questions information for a given attempt.
 953       *
 954       * @param  quiz_attempt  $attemptobj  the quiz attempt object
 955       * @param  bool  $review  whether if we are in review mode or not
 956       * @param  mixed  $page  string 'all' or integer page number
 957       * @return array array of questions including data
 958       */
 959      private static function get_attempt_questions_data(quiz_attempt $attemptobj, $review, $page = 'all') {
 960          global $PAGE;
 961  
 962          $questions = array();
 963          $contextid = $attemptobj->get_quizobj()->get_context()->id;
 964          $displayoptions = $attemptobj->get_display_options($review);
 965          $renderer = $PAGE->get_renderer('mod_quiz');
 966          $contextid = $attemptobj->get_quizobj()->get_context()->id;
 967  
 968          foreach ($attemptobj->get_slots($page) as $slot) {
 969              $qtype = $attemptobj->get_question_type_name($slot);
 970              $qattempt = $attemptobj->get_question_attempt($slot);
 971              $questiondef = $qattempt->get_question(true);
 972  
 973              // Get response files (for questions like essay that allows attachments).
 974              $responsefileareas = [];
 975              foreach (question_bank::get_qtype($qtype)->response_file_areas() as $area) {
 976                  if ($files = $attemptobj->get_question_attempt($slot)->get_last_qt_files($area, $contextid)) {
 977                      $responsefileareas[$area]['area'] = $area;
 978                      $responsefileareas[$area]['files'] = [];
 979  
 980                      foreach ($files as $file) {
 981                          $responsefileareas[$area]['files'][] = array(
 982                              'filename' => $file->get_filename(),
 983                              'fileurl' => $qattempt->get_response_file_url($file),
 984                              'filesize' => $file->get_filesize(),
 985                              'filepath' => $file->get_filepath(),
 986                              'mimetype' => $file->get_mimetype(),
 987                              'timemodified' => $file->get_timemodified(),
 988                          );
 989                      }
 990                  }
 991              }
 992  
 993              // Check display settings for question.
 994              $settings = $questiondef->get_question_definition_for_external_rendering($qattempt, $displayoptions);
 995  
 996              $question = array(
 997                  'slot' => $slot,
 998                  'type' => $qtype,
 999                  'page' => $attemptobj->get_question_page($slot),
1000                  'flagged' => $attemptobj->is_question_flagged($slot),
1001                  'html' => $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code(),
1002                  'responsefileareas' => $responsefileareas,
1003                  'sequencecheck' => $qattempt->get_sequence_check_count(),
1004                  'lastactiontime' => $qattempt->get_last_step()->get_timecreated(),
1005                  'hasautosavedstep' => $qattempt->has_autosaved_step(),
1006                  'settings' => !empty($settings) ? json_encode($settings) : null,
1007              );
1008  
1009              if ($attemptobj->is_real_question($slot)) {
1010                  $question['number'] = $attemptobj->get_question_number($slot);
1011                  $showcorrectness = $displayoptions->correctness && $qattempt->has_marks();
1012                  if ($showcorrectness) {
1013                      $question['state'] = (string) $attemptobj->get_question_state($slot);
1014                  }
1015                  $question['status'] = $attemptobj->get_question_status($slot, $displayoptions->correctness);
1016                  $question['blockedbyprevious'] = $attemptobj->is_blocked_by_previous_question($slot);
1017              }
1018              if ($displayoptions->marks >= question_display_options::MAX_ONLY) {
1019                  $question['maxmark'] = $qattempt->get_max_mark();
1020              }
1021              if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) {
1022                  $question['mark'] = $attemptobj->get_question_mark($slot);
1023              }
1024              if ($attemptobj->check_page_access($attemptobj->get_question_page($slot), false)) {
1025                  $questions[] = $question;
1026              }
1027          }
1028          return $questions;
1029      }
1030  
1031      /**
1032       * Describes the parameters for get_attempt_data.
1033       *
1034       * @return external_function_parameters
1035       * @since Moodle 3.1
1036       */
1037      public static function get_attempt_data_parameters() {
1038          return new external_function_parameters (
1039              array(
1040                  'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1041                  'page' => new external_value(PARAM_INT, 'page number'),
1042                  'preflightdata' => new external_multiple_structure(
1043                      new external_single_structure(
1044                          array(
1045                              'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1046                              'value' => new external_value(PARAM_RAW, 'data value'),
1047                          )
1048                      ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1049                  )
1050              )
1051          );
1052      }
1053  
1054      /**
1055       * Returns information for the given attempt page for a quiz attempt in progress.
1056       *
1057       * @param int $attemptid attempt id
1058       * @param int $page page number
1059       * @param array $preflightdata preflight required data (like passwords)
1060       * @return array of warnings and the attempt data, next page, message and questions
1061       * @since Moodle 3.1
1062       * @throws moodle_quiz_exceptions
1063       */
1064      public static function get_attempt_data($attemptid, $page, $preflightdata = array()) {
1065  
1066          $warnings = array();
1067  
1068          $params = array(
1069              'attemptid' => $attemptid,
1070              'page' => $page,
1071              'preflightdata' => $preflightdata,
1072          );
1073          $params = self::validate_parameters(self::get_attempt_data_parameters(), $params);
1074  
1075          list($attemptobj, $messages) = self::validate_attempt($params);
1076  
1077          if ($attemptobj->is_last_page($params['page'])) {
1078              $nextpage = -1;
1079          } else {
1080              $nextpage = $params['page'] + 1;
1081          }
1082  
1083          $result = array();
1084          $result['attempt'] = $attemptobj->get_attempt();
1085          $result['messages'] = $messages;
1086          $result['nextpage'] = $nextpage;
1087          $result['warnings'] = $warnings;
1088          $result['questions'] = self::get_attempt_questions_data($attemptobj, false, $params['page']);
1089  
1090          return $result;
1091      }
1092  
1093      /**
1094       * Describes the get_attempt_data return value.
1095       *
1096       * @return external_single_structure
1097       * @since Moodle 3.1
1098       */
1099      public static function get_attempt_data_returns() {
1100          return new external_single_structure(
1101              array(
1102                  'attempt' => self::attempt_structure(),
1103                  'messages' => new external_multiple_structure(
1104                      new external_value(PARAM_TEXT, 'access message'),
1105                      'access messages, will only be returned for users with mod/quiz:preview capability,
1106                      for other users this method will throw an exception if there are messages'),
1107                  'nextpage' => new external_value(PARAM_INT, 'next page number'),
1108                  'questions' => new external_multiple_structure(self::question_structure()),
1109                  'warnings' => new external_warnings(),
1110              )
1111          );
1112      }
1113  
1114      /**
1115       * Describes the parameters for get_attempt_summary.
1116       *
1117       * @return external_function_parameters
1118       * @since Moodle 3.1
1119       */
1120      public static function get_attempt_summary_parameters() {
1121          return new external_function_parameters (
1122              array(
1123                  'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1124                  'preflightdata' => new external_multiple_structure(
1125                      new external_single_structure(
1126                          array(
1127                              'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1128                              'value' => new external_value(PARAM_RAW, 'data value'),
1129                          )
1130                      ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1131                  )
1132              )
1133          );
1134      }
1135  
1136      /**
1137       * Returns a summary of a quiz attempt before it is submitted.
1138       *
1139       * @param int $attemptid attempt id
1140       * @param int $preflightdata preflight required data (like passwords)
1141       * @return array of warnings and the attempt summary data for each question
1142       * @since Moodle 3.1
1143       */
1144      public static function get_attempt_summary($attemptid, $preflightdata = array()) {
1145  
1146          $warnings = array();
1147  
1148          $params = array(
1149              'attemptid' => $attemptid,
1150              'preflightdata' => $preflightdata,
1151          );
1152          $params = self::validate_parameters(self::get_attempt_summary_parameters(), $params);
1153  
1154          list($attemptobj, $messages) = self::validate_attempt($params, true, false);
1155  
1156          $result = array();
1157          $result['warnings'] = $warnings;
1158          $result['questions'] = self::get_attempt_questions_data($attemptobj, false, 'all');
1159  
1160          return $result;
1161      }
1162  
1163      /**
1164       * Describes the get_attempt_summary return value.
1165       *
1166       * @return external_single_structure
1167       * @since Moodle 3.1
1168       */
1169      public static function get_attempt_summary_returns() {
1170          return new external_single_structure(
1171              array(
1172                  'questions' => new external_multiple_structure(self::question_structure()),
1173                  'warnings' => new external_warnings(),
1174              )
1175          );
1176      }
1177  
1178      /**
1179       * Describes the parameters for save_attempt.
1180       *
1181       * @return external_function_parameters
1182       * @since Moodle 3.1
1183       */
1184      public static function save_attempt_parameters() {
1185          return new external_function_parameters (
1186              array(
1187                  'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1188                  'data' => new external_multiple_structure(
1189                      new external_single_structure(
1190                          array(
1191                              'name' => new external_value(PARAM_RAW, 'data name'),
1192                              'value' => new external_value(PARAM_RAW, 'data value'),
1193                          )
1194                      ), 'the data to be saved'
1195                  ),
1196                  'preflightdata' => new external_multiple_structure(
1197                      new external_single_structure(
1198                          array(
1199                              'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1200                              'value' => new external_value(PARAM_RAW, 'data value'),
1201                          )
1202                      ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1203                  )
1204              )
1205          );
1206      }
1207  
1208      /**
1209       * Processes save requests during the quiz. This function is intended for the quiz auto-save feature.
1210       *
1211       * @param int $attemptid attempt id
1212       * @param array $data the data to be saved
1213       * @param  array $preflightdata preflight required data (like passwords)
1214       * @return array of warnings and execution result
1215       * @since Moodle 3.1
1216       */
1217      public static function save_attempt($attemptid, $data, $preflightdata = array()) {
1218          global $DB, $USER;
1219  
1220          $warnings = array();
1221  
1222          $params = array(
1223              'attemptid' => $attemptid,
1224              'data' => $data,
1225              'preflightdata' => $preflightdata,
1226          );
1227          $params = self::validate_parameters(self::save_attempt_parameters(), $params);
1228  
1229          // Add a page, required by validate_attempt.
1230          list($attemptobj, $messages) = self::validate_attempt($params);
1231  
1232          // Prevent functions like file_get_submitted_draft_itemid() or form library requiring a sesskey for WS requests.
1233          if (WS_SERVER || PHPUNIT_TEST) {
1234              $USER->ignoresesskey = true;
1235          }
1236          $transaction = $DB->start_delegated_transaction();
1237          // Create the $_POST object required by the question engine.
1238          $_POST = array();
1239          foreach ($data as $element) {
1240              $_POST[$element['name']] = $element['value'];
1241              // Some deep core functions like file_get_submitted_draft_itemid() also requires $_REQUEST to be filled.
1242              $_REQUEST[$element['name']] = $element['value'];
1243          }
1244          $timenow = time();
1245          // Update the timemodifiedoffline field.
1246          $attemptobj->set_offline_modified_time($timenow);
1247          $attemptobj->process_auto_save($timenow);
1248          $transaction->allow_commit();
1249  
1250          $result = array();
1251          $result['status'] = true;
1252          $result['warnings'] = $warnings;
1253          return $result;
1254      }
1255  
1256      /**
1257       * Describes the save_attempt return value.
1258       *
1259       * @return external_single_structure
1260       * @since Moodle 3.1
1261       */
1262      public static function save_attempt_returns() {
1263          return new external_single_structure(
1264              array(
1265                  'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1266                  'warnings' => new external_warnings(),
1267              )
1268          );
1269      }
1270  
1271      /**
1272       * Describes the parameters for process_attempt.
1273       *
1274       * @return external_function_parameters
1275       * @since Moodle 3.1
1276       */
1277      public static function process_attempt_parameters() {
1278          return new external_function_parameters (
1279              array(
1280                  'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1281                  'data' => new external_multiple_structure(
1282                      new external_single_structure(
1283                          array(
1284                              'name' => new external_value(PARAM_RAW, 'data name'),
1285                              'value' => new external_value(PARAM_RAW, 'data value'),
1286                          )
1287                      ),
1288                      'the data to be saved', VALUE_DEFAULT, array()
1289                  ),
1290                  'finishattempt' => new external_value(PARAM_BOOL, 'whether to finish or not the attempt', VALUE_DEFAULT, false),
1291                  'timeup' => new external_value(PARAM_BOOL, 'whether the WS was called by a timer when the time is up',
1292                                                  VALUE_DEFAULT, false),
1293                  'preflightdata' => new external_multiple_structure(
1294                      new external_single_structure(
1295                          array(
1296                              'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1297                              'value' => new external_value(PARAM_RAW, 'data value'),
1298                          )
1299                      ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1300                  )
1301              )
1302          );
1303      }
1304  
1305      /**
1306       * Process responses during an attempt at a quiz and also deals with attempts finishing.
1307       *
1308       * @param int $attemptid attempt id
1309       * @param array $data the data to be saved
1310       * @param bool $finishattempt whether to finish or not the attempt
1311       * @param bool $timeup whether the WS was called by a timer when the time is up
1312       * @param array $preflightdata preflight required data (like passwords)
1313       * @return array of warnings and the attempt state after the processing
1314       * @since Moodle 3.1
1315       */
1316      public static function process_attempt($attemptid, $data, $finishattempt = false, $timeup = false, $preflightdata = array()) {
1317          global $USER;
1318  
1319          $warnings = array();
1320  
1321          $params = array(
1322              'attemptid' => $attemptid,
1323              'data' => $data,
1324              'finishattempt' => $finishattempt,
1325              'timeup' => $timeup,
1326              'preflightdata' => $preflightdata,
1327          );
1328          $params = self::validate_parameters(self::process_attempt_parameters(), $params);
1329  
1330          // Do not check access manager rules and evaluate fail if overdue.
1331          $attemptobj = quiz_attempt::create($params['attemptid']);
1332          $failifoverdue = !($attemptobj->get_quizobj()->get_quiz()->overduehandling == 'graceperiod');
1333  
1334          list($attemptobj, $messages) = self::validate_attempt($params, false, $failifoverdue);
1335  
1336          // Prevent functions like file_get_submitted_draft_itemid() or form library requiring a sesskey for WS requests.
1337          if (WS_SERVER || PHPUNIT_TEST) {
1338              $USER->ignoresesskey = true;
1339          }
1340          // Create the $_POST object required by the question engine.
1341          $_POST = array();
1342          foreach ($params['data'] as $element) {
1343              $_POST[$element['name']] = $element['value'];
1344              $_REQUEST[$element['name']] = $element['value'];
1345          }
1346          $timenow = time();
1347          $finishattempt = $params['finishattempt'];
1348          $timeup = $params['timeup'];
1349  
1350          $result = array();
1351          // Update the timemodifiedoffline field.
1352          $attemptobj->set_offline_modified_time($timenow);
1353          $result['state'] = $attemptobj->process_attempt($timenow, $finishattempt, $timeup, 0);
1354  
1355          $result['warnings'] = $warnings;
1356          return $result;
1357      }
1358  
1359      /**
1360       * Describes the process_attempt return value.
1361       *
1362       * @return external_single_structure
1363       * @since Moodle 3.1
1364       */
1365      public static function process_attempt_returns() {
1366          return new external_single_structure(
1367              array(
1368                  'state' => new external_value(PARAM_ALPHANUMEXT, 'state: the new attempt state:
1369                                                                      inprogress, finished, overdue, abandoned'),
1370                  'warnings' => new external_warnings(),
1371              )
1372          );
1373      }
1374  
1375      /**
1376       * Validate an attempt finished for review. The attempt would be reviewed by a user or a teacher.
1377       *
1378       * @param  array $params Array of parameters including the attemptid
1379       * @return  array containing the attempt object and display options
1380       * @since  Moodle 3.1
1381       * @throws  moodle_exception
1382       * @throws  moodle_quiz_exception
1383       */
1384      protected static function validate_attempt_review($params) {
1385  
1386          $attemptobj = quiz_attempt::create($params['attemptid']);
1387          $attemptobj->check_review_capability();
1388  
1389          $displayoptions = $attemptobj->get_display_options(true);
1390          if ($attemptobj->is_own_attempt()) {
1391              if (!$attemptobj->is_finished()) {
1392                  throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptclosed');
1393              } else if (!$displayoptions->attempt) {
1394                  throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreview', null, '',
1395                      $attemptobj->cannot_review_message());
1396              }
1397          } else if (!$attemptobj->is_review_allowed()) {
1398              throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreviewattempt');
1399          }
1400          return array($attemptobj, $displayoptions);
1401      }
1402  
1403      /**
1404       * Describes the parameters for get_attempt_review.
1405       *
1406       * @return external_function_parameters
1407       * @since Moodle 3.1
1408       */
1409      public static function get_attempt_review_parameters() {
1410          return new external_function_parameters (
1411              array(
1412                  'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1413                  'page' => new external_value(PARAM_INT, 'page number, empty for all the questions in all the pages',
1414                                                  VALUE_DEFAULT, -1),
1415              )
1416          );
1417      }
1418  
1419      /**
1420       * Returns review information for the given finished attempt, can be used by users or teachers.
1421       *
1422       * @param int $attemptid attempt id
1423       * @param int $page page number, empty for all the questions in all the pages
1424       * @return array of warnings and the attempt data, feedback and questions
1425       * @since Moodle 3.1
1426       * @throws  moodle_exception
1427       * @throws  moodle_quiz_exception
1428       */
1429      public static function get_attempt_review($attemptid, $page = -1) {
1430          global $PAGE;
1431  
1432          $warnings = array();
1433  
1434          $params = array(
1435              'attemptid' => $attemptid,
1436              'page' => $page,
1437          );
1438          $params = self::validate_parameters(self::get_attempt_review_parameters(), $params);
1439  
1440          list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1441  
1442          if ($params['page'] !== -1) {
1443              $page = $attemptobj->force_page_number_into_range($params['page']);
1444          } else {
1445              $page = 'all';
1446          }
1447  
1448          // Prepare the output.
1449          $result = array();
1450          $result['attempt'] = $attemptobj->get_attempt();
1451          $result['questions'] = self::get_attempt_questions_data($attemptobj, true, $page, true);
1452  
1453          $result['additionaldata'] = array();
1454          // Summary data (from behaviours).
1455          $summarydata = $attemptobj->get_additional_summary_data($displayoptions);
1456          foreach ($summarydata as $key => $data) {
1457              // This text does not need formatting (no need for external_format_[string|text]).
1458              $result['additionaldata'][] = array(
1459                  'id' => $key,
1460                  'title' => $data['title'], $attemptobj->get_quizobj()->get_context()->id,
1461                  'content' => $data['content'],
1462              );
1463          }
1464  
1465          // Feedback if there is any, and the user is allowed to see it now.
1466          $grade = quiz_rescale_grade($attemptobj->get_attempt()->sumgrades, $attemptobj->get_quiz(), false);
1467  
1468          $feedback = $attemptobj->get_overall_feedback($grade);
1469          if ($displayoptions->overallfeedback && $feedback) {
1470              $result['additionaldata'][] = array(
1471                  'id' => 'feedback',
1472                  'title' => get_string('feedback', 'quiz'),
1473                  'content' => $feedback,
1474              );
1475          }
1476  
1477          $result['grade'] = $grade;
1478          $result['warnings'] = $warnings;
1479          return $result;
1480      }
1481  
1482      /**
1483       * Describes the get_attempt_review return value.
1484       *
1485       * @return external_single_structure
1486       * @since Moodle 3.1
1487       */
1488      public static function get_attempt_review_returns() {
1489          return new external_single_structure(
1490              array(
1491                  'grade' => new external_value(PARAM_RAW, 'grade for the quiz (or empty or "notyetgraded")'),
1492                  'attempt' => self::attempt_structure(),
1493                  'additionaldata' => new external_multiple_structure(
1494                      new external_single_structure(
1495                          array(
1496                              'id' => new external_value(PARAM_ALPHANUMEXT, 'id of the data'),
1497                              'title' => new external_value(PARAM_TEXT, 'data title'),
1498                              'content' => new external_value(PARAM_RAW, 'data content'),
1499                          )
1500                      )
1501                  ),
1502                  'questions' => new external_multiple_structure(self::question_structure()),
1503                  'warnings' => new external_warnings(),
1504              )
1505          );
1506      }
1507  
1508      /**
1509       * Describes the parameters for view_attempt.
1510       *
1511       * @return external_function_parameters
1512       * @since Moodle 3.1
1513       */
1514      public static function view_attempt_parameters() {
1515          return new external_function_parameters (
1516              array(
1517                  'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1518                  'page' => new external_value(PARAM_INT, 'page number'),
1519                  'preflightdata' => new external_multiple_structure(
1520                      new external_single_structure(
1521                          array(
1522                              'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1523                              'value' => new external_value(PARAM_RAW, 'data value'),
1524                          )
1525                      ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1526                  )
1527              )
1528          );
1529      }
1530  
1531      /**
1532       * Trigger the attempt viewed event.
1533       *
1534       * @param int $attemptid attempt id
1535       * @param int $page page number
1536       * @param array $preflightdata preflight required data (like passwords)
1537       * @return array of warnings and status result
1538       * @since Moodle 3.1
1539       */
1540      public static function view_attempt($attemptid, $page, $preflightdata = array()) {
1541  
1542          $warnings = array();
1543  
1544          $params = array(
1545              'attemptid' => $attemptid,
1546              'page' => $page,
1547              'preflightdata' => $preflightdata,
1548          );
1549          $params = self::validate_parameters(self::view_attempt_parameters(), $params);
1550          list($attemptobj, $messages) = self::validate_attempt($params);
1551  
1552          // Log action.
1553          $attemptobj->fire_attempt_viewed_event();
1554  
1555          // Update attempt page, throwing an exception if $page is not valid.
1556          if (!$attemptobj->set_currentpage($params['page'])) {
1557              throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
1558          }
1559  
1560          $result = array();
1561          $result['status'] = true;
1562          $result['warnings'] = $warnings;
1563          return $result;
1564      }
1565  
1566      /**
1567       * Describes the view_attempt return value.
1568       *
1569       * @return external_single_structure
1570       * @since Moodle 3.1
1571       */
1572      public static function view_attempt_returns() {
1573          return new external_single_structure(
1574              array(
1575                  'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1576                  'warnings' => new external_warnings(),
1577              )
1578          );
1579      }
1580  
1581      /**
1582       * Describes the parameters for view_attempt_summary.
1583       *
1584       * @return external_function_parameters
1585       * @since Moodle 3.1
1586       */
1587      public static function view_attempt_summary_parameters() {
1588          return new external_function_parameters (
1589              array(
1590                  'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1591                  'preflightdata' => new external_multiple_structure(
1592                      new external_single_structure(
1593                          array(
1594                              'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1595                              'value' => new external_value(PARAM_RAW, 'data value'),
1596                          )
1597                      ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1598                  )
1599              )
1600          );
1601      }
1602  
1603      /**
1604       * Trigger the attempt summary viewed event.
1605       *
1606       * @param int $attemptid attempt id
1607       * @param array $preflightdata preflight required data (like passwords)
1608       * @return array of warnings and status result
1609       * @since Moodle 3.1
1610       */
1611      public static function view_attempt_summary($attemptid, $preflightdata = array()) {
1612  
1613          $warnings = array();
1614  
1615          $params = array(
1616              'attemptid' => $attemptid,
1617              'preflightdata' => $preflightdata,
1618          );
1619          $params = self::validate_parameters(self::view_attempt_summary_parameters(), $params);
1620          list($attemptobj, $messages) = self::validate_attempt($params);
1621  
1622          // Log action.
1623          $attemptobj->fire_attempt_summary_viewed_event();
1624  
1625          $result = array();
1626          $result['status'] = true;
1627          $result['warnings'] = $warnings;
1628          return $result;
1629      }
1630  
1631      /**
1632       * Describes the view_attempt_summary return value.
1633       *
1634       * @return external_single_structure
1635       * @since Moodle 3.1
1636       */
1637      public static function view_attempt_summary_returns() {
1638          return new external_single_structure(
1639              array(
1640                  'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1641                  'warnings' => new external_warnings(),
1642              )
1643          );
1644      }
1645  
1646      /**
1647       * Describes the parameters for view_attempt_review.
1648       *
1649       * @return external_function_parameters
1650       * @since Moodle 3.1
1651       */
1652      public static function view_attempt_review_parameters() {
1653          return new external_function_parameters (
1654              array(
1655                  'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1656              )
1657          );
1658      }
1659  
1660      /**
1661       * Trigger the attempt reviewed event.
1662       *
1663       * @param int $attemptid attempt id
1664       * @return array of warnings and status result
1665       * @since Moodle 3.1
1666       */
1667      public static function view_attempt_review($attemptid) {
1668  
1669          $warnings = array();
1670  
1671          $params = array(
1672              'attemptid' => $attemptid,
1673          );
1674          $params = self::validate_parameters(self::view_attempt_review_parameters(), $params);
1675          list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1676  
1677          // Log action.
1678          $attemptobj->fire_attempt_reviewed_event();
1679  
1680          $result = array();
1681          $result['status'] = true;
1682          $result['warnings'] = $warnings;
1683          return $result;
1684      }
1685  
1686      /**
1687       * Describes the view_attempt_review return value.
1688       *
1689       * @return external_single_structure
1690       * @since Moodle 3.1
1691       */
1692      public static function view_attempt_review_returns() {
1693          return new external_single_structure(
1694              array(
1695                  'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1696                  'warnings' => new external_warnings(),
1697              )
1698          );
1699      }
1700  
1701      /**
1702       * Describes the parameters for view_quiz.
1703       *
1704       * @return external_function_parameters
1705       * @since Moodle 3.1
1706       */
1707      public static function get_quiz_feedback_for_grade_parameters() {
1708          return new external_function_parameters (
1709              array(
1710                  'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1711                  'grade' => new external_value(PARAM_FLOAT, 'the grade to check'),
1712              )
1713          );
1714      }
1715  
1716      /**
1717       * Get the feedback text that should be show to a student who got the given grade in the given quiz.
1718       *
1719       * @param int $quizid quiz instance id
1720       * @param float $grade the grade to check
1721       * @return array of warnings and status result
1722       * @since Moodle 3.1
1723       * @throws moodle_exception
1724       */
1725      public static function get_quiz_feedback_for_grade($quizid, $grade) {
1726          global $DB;
1727  
1728          $params = array(
1729              'quizid' => $quizid,
1730              'grade' => $grade,
1731          );
1732          $params = self::validate_parameters(self::get_quiz_feedback_for_grade_parameters(), $params);
1733          $warnings = array();
1734  
1735          list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1736  
1737          $result = array();
1738          $result['feedbacktext'] = '';
1739          $result['feedbacktextformat'] = FORMAT_MOODLE;
1740  
1741          $feedback = quiz_feedback_record_for_grade($params['grade'], $quiz);
1742          if (!empty($feedback->feedbacktext)) {
1743              list($text, $format) = external_format_text($feedback->feedbacktext, $feedback->feedbacktextformat, $context->id,
1744                                                          'mod_quiz', 'feedback', $feedback->id);
1745              $result['feedbacktext'] = $text;
1746              $result['feedbacktextformat'] = $format;
1747              $feedbackinlinefiles = external_util::get_area_files($context->id, 'mod_quiz', 'feedback', $feedback->id);
1748              if (!empty($feedbackinlinefiles)) {
1749                  $result['feedbackinlinefiles'] = $feedbackinlinefiles;
1750              }
1751          }
1752  
1753          $result['warnings'] = $warnings;
1754          return $result;
1755      }
1756  
1757      /**
1758       * Describes the get_quiz_feedback_for_grade return value.
1759       *
1760       * @return external_single_structure
1761       * @since Moodle 3.1
1762       */
1763      public static function get_quiz_feedback_for_grade_returns() {
1764          return new external_single_structure(
1765              array(
1766                  'feedbacktext' => new external_value(PARAM_RAW, 'the comment that corresponds to this grade (empty for none)'),
1767                  'feedbacktextformat' => new external_format_value('feedbacktext', VALUE_OPTIONAL),
1768                  'feedbackinlinefiles' => new external_files('feedback inline files', VALUE_OPTIONAL),
1769                  'warnings' => new external_warnings(),
1770              )
1771          );
1772      }
1773  
1774      /**
1775       * Describes the parameters for get_quiz_access_information.
1776       *
1777       * @return external_function_parameters
1778       * @since Moodle 3.1
1779       */
1780      public static function get_quiz_access_information_parameters() {
1781          return new external_function_parameters (
1782              array(
1783                  'quizid' => new external_value(PARAM_INT, 'quiz instance id')
1784              )
1785          );
1786      }
1787  
1788      /**
1789       * Return access information for a given quiz.
1790       *
1791       * @param int $quizid quiz instance id
1792       * @return array of warnings and the access information
1793       * @since Moodle 3.1
1794       * @throws  moodle_quiz_exception
1795       */
1796      public static function get_quiz_access_information($quizid) {
1797          global $DB, $USER;
1798  
1799          $warnings = array();
1800  
1801          $params = array(
1802              'quizid' => $quizid
1803          );
1804          $params = self::validate_parameters(self::get_quiz_access_information_parameters(), $params);
1805  
1806          list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1807  
1808          $result = array();
1809          // Capabilities first.
1810          $result['canattempt'] = has_capability('mod/quiz:attempt', $context);;
1811          $result['canmanage'] = has_capability('mod/quiz:manage', $context);;
1812          $result['canpreview'] = has_capability('mod/quiz:preview', $context);;
1813          $result['canreviewmyattempts'] = has_capability('mod/quiz:reviewmyattempts', $context);;
1814          $result['canviewreports'] = has_capability('mod/quiz:viewreports', $context);;
1815  
1816          // Access manager now.
1817          $quizobj = quiz::create($cm->instance, $USER->id);
1818          $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1819          $timenow = time();
1820          $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits);
1821  
1822          $result['accessrules'] = $accessmanager->describe_rules();
1823          $result['activerulenames'] = $accessmanager->get_active_rule_names();
1824          $result['preventaccessreasons'] = $accessmanager->prevent_access();
1825  
1826          $result['warnings'] = $warnings;
1827          return $result;
1828      }
1829  
1830      /**
1831       * Describes the get_quiz_access_information return value.
1832       *
1833       * @return external_single_structure
1834       * @since Moodle 3.1
1835       */
1836      public static function get_quiz_access_information_returns() {
1837          return new external_single_structure(
1838              array(
1839                  'canattempt' => new external_value(PARAM_BOOL, 'Whether the user can do the quiz or not.'),
1840                  'canmanage' => new external_value(PARAM_BOOL, 'Whether the user can edit the quiz settings or not.'),
1841                  'canpreview' => new external_value(PARAM_BOOL, 'Whether the user can preview the quiz or not.'),
1842                  'canreviewmyattempts' => new external_value(PARAM_BOOL, 'Whether the users can review their previous attempts
1843                                                                  or not.'),
1844                  'canviewreports' => new external_value(PARAM_BOOL, 'Whether the user can view the quiz reports or not.'),
1845                  'accessrules' => new external_multiple_structure(
1846                                      new external_value(PARAM_TEXT, 'rule description'), 'list of rules'),
1847                  'activerulenames' => new external_multiple_structure(
1848                                      new external_value(PARAM_PLUGIN, 'rule plugin names'), 'list of active rules'),
1849                  'preventaccessreasons' => new external_multiple_structure(
1850                                              new external_value(PARAM_TEXT, 'access restriction description'), 'list of reasons'),
1851                  'warnings' => new external_warnings(),
1852              )
1853          );
1854      }
1855  
1856      /**
1857       * Describes the parameters for get_attempt_access_information.
1858       *
1859       * @return external_function_parameters
1860       * @since Moodle 3.1
1861       */
1862      public static function get_attempt_access_information_parameters() {
1863          return new external_function_parameters (
1864              array(
1865                  'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1866                  'attemptid' => new external_value(PARAM_INT, 'attempt id, 0 for the user last attempt if exists', VALUE_DEFAULT, 0),
1867              )
1868          );
1869      }
1870  
1871      /**
1872       * Return access information for a given attempt in a quiz.
1873       *
1874       * @param int $quizid quiz instance id
1875       * @param int $attemptid attempt id, 0 for the user last attempt if exists
1876       * @return array of warnings and the access information
1877       * @since Moodle 3.1
1878       * @throws  moodle_quiz_exception
1879       */
1880      public static function get_attempt_access_information($quizid, $attemptid = 0) {
1881          global $DB, $USER;
1882  
1883          $warnings = array();
1884  
1885          $params = array(
1886              'quizid' => $quizid,
1887              'attemptid' => $attemptid,
1888          );
1889          $params = self::validate_parameters(self::get_attempt_access_information_parameters(), $params);
1890  
1891          list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1892  
1893          $attempttocheck = 0;
1894          if (!empty($params['attemptid'])) {
1895              $attemptobj = quiz_attempt::create($params['attemptid']);
1896              if ($attemptobj->get_userid() != $USER->id) {
1897                  throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
1898              }
1899              $attempttocheck = $attemptobj->get_attempt();
1900          }
1901  
1902          // Access manager now.
1903          $quizobj = quiz::create($cm->instance, $USER->id);
1904          $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1905          $timenow = time();
1906          $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits);
1907  
1908          $attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'finished', true);
1909          $lastfinishedattempt = end($attempts);
1910          if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
1911              $attempts[] = $unfinishedattempt;
1912  
1913              // Check if the attempt is now overdue. In that case the state will change.
1914              $quizobj->create_attempt_object($unfinishedattempt)->handle_if_time_expired(time(), false);
1915  
1916              if ($unfinishedattempt->state != quiz_attempt::IN_PROGRESS and $unfinishedattempt->state != quiz_attempt::OVERDUE) {
1917                  $lastfinishedattempt = $unfinishedattempt;
1918              }
1919          }
1920          $numattempts = count($attempts);
1921  
1922          if (!$attempttocheck) {
1923              $attempttocheck = $unfinishedattempt ? $unfinishedattempt : $lastfinishedattempt;
1924          }
1925  
1926          $result = array();
1927          $result['isfinished'] = $accessmanager->is_finished($numattempts, $lastfinishedattempt);
1928          $result['preventnewattemptreasons'] = $accessmanager->prevent_new_attempt($numattempts, $lastfinishedattempt);
1929  
1930          if ($attempttocheck) {
1931              $endtime = $accessmanager->get_end_time($attempttocheck);
1932              $result['endtime'] = ($endtime === false) ? 0 : $endtime;
1933              $attemptid = $unfinishedattempt ? $unfinishedattempt->id : null;
1934              $result['ispreflightcheckrequired'] = $accessmanager->is_preflight_check_required($attemptid);
1935          }
1936  
1937          $result['warnings'] = $warnings;
1938          return $result;
1939      }
1940  
1941      /**
1942       * Describes the get_attempt_access_information return value.
1943       *
1944       * @return external_single_structure
1945       * @since Moodle 3.1
1946       */
1947      public static function get_attempt_access_information_returns() {
1948          return new external_single_structure(
1949              array(
1950                  'endtime' => new external_value(PARAM_INT, 'When the attempt must be submitted (determined by rules).',
1951                                                  VALUE_OPTIONAL),
1952                  'isfinished' => new external_value(PARAM_BOOL, 'Whether there is no way the user will ever be allowed to attempt.'),
1953                  'ispreflightcheckrequired' => new external_value(PARAM_BOOL, 'whether a check is required before the user
1954                                                                      starts/continues his attempt.', VALUE_OPTIONAL),
1955                  'preventnewattemptreasons' => new external_multiple_structure(
1956                                                  new external_value(PARAM_TEXT, 'access restriction description'),
1957                                                                      'list of reasons'),
1958                  'warnings' => new external_warnings(),
1959              )
1960          );
1961      }
1962  
1963      /**
1964       * Describes the parameters for get_quiz_required_qtypes.
1965       *
1966       * @return external_function_parameters
1967       * @since Moodle 3.1
1968       */
1969      public static function get_quiz_required_qtypes_parameters() {
1970          return new external_function_parameters (
1971              array(
1972                  'quizid' => new external_value(PARAM_INT, 'quiz instance id')
1973              )
1974          );
1975      }
1976  
1977      /**
1978       * Return the potential question types that would be required for a given quiz.
1979       * Please note that for random question types we return the potential question types in the category choosen.
1980       *
1981       * @param int $quizid quiz instance id
1982       * @return array of warnings and the access information
1983       * @since Moodle 3.1
1984       * @throws  moodle_quiz_exception
1985       */
1986      public static function get_quiz_required_qtypes($quizid) {
1987          global $DB, $USER;
1988  
1989          $warnings = array();
1990  
1991          $params = array(
1992              'quizid' => $quizid
1993          );
1994          $params = self::validate_parameters(self::get_quiz_required_qtypes_parameters(), $params);
1995  
1996          list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1997  
1998          $quizobj = quiz::create($cm->instance, $USER->id);
1999          $quizobj->preload_questions();
2000          $quizobj->load_questions();
2001  
2002          // Question types used.
2003          $result = array();
2004          $result['questiontypes'] = $quizobj->get_all_question_types_used(true);
2005          $result['warnings'] = $warnings;
2006          return $result;
2007      }
2008  
2009      /**
2010       * Describes the get_quiz_required_qtypes return value.
2011       *
2012       * @return external_single_structure
2013       * @since Moodle 3.1
2014       */
2015      public static function get_quiz_required_qtypes_returns() {
2016          return new external_single_structure(
2017              array(
2018                  'questiontypes' => new external_multiple_structure(
2019                                      new external_value(PARAM_PLUGIN, 'question type'), 'list of question types used in the quiz'),
2020                  'warnings' => new external_warnings(),
2021              )
2022          );
2023      }
2024  
2025  }