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