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