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