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