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 * Library of functions for the quiz module. 19 * 20 * This contains functions that are called also from outside the quiz module 21 * Functions that are only called by the quiz module itself are in {@link locallib.php} 22 * 23 * @package mod_quiz 24 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 */ 27 28 29 defined('MOODLE_INTERNAL') || die(); 30 31 use core_question\statistics\questions\all_calculated_for_qubaid_condition; 32 33 require_once($CFG->dirroot . '/calendar/lib.php'); 34 35 36 /**#@+ 37 * Option controlling what options are offered on the quiz settings form. 38 */ 39 define('QUIZ_MAX_ATTEMPT_OPTION', 10); 40 define('QUIZ_MAX_QPP_OPTION', 50); 41 define('QUIZ_MAX_DECIMAL_OPTION', 5); 42 define('QUIZ_MAX_Q_DECIMAL_OPTION', 7); 43 /**#@-*/ 44 45 /**#@+ 46 * Options determining how the grades from individual attempts are combined to give 47 * the overall grade for a user 48 */ 49 define('QUIZ_GRADEHIGHEST', '1'); 50 define('QUIZ_GRADEAVERAGE', '2'); 51 define('QUIZ_ATTEMPTFIRST', '3'); 52 define('QUIZ_ATTEMPTLAST', '4'); 53 /**#@-*/ 54 55 /** 56 * @var int If start and end date for the quiz are more than this many seconds apart 57 * they will be represented by two separate events in the calendar 58 */ 59 define('QUIZ_MAX_EVENT_LENGTH', 5*24*60*60); // 5 days. 60 61 /**#@+ 62 * Options for navigation method within quizzes. 63 */ 64 define('QUIZ_NAVMETHOD_FREE', 'free'); 65 define('QUIZ_NAVMETHOD_SEQ', 'sequential'); 66 /**#@-*/ 67 68 /** 69 * Event types. 70 */ 71 define('QUIZ_EVENT_TYPE_OPEN', 'open'); 72 define('QUIZ_EVENT_TYPE_CLOSE', 'close'); 73 74 require_once (__DIR__ . '/deprecatedlib.php'); 75 76 /** 77 * Given an object containing all the necessary data, 78 * (defined by the form in mod_form.php) this function 79 * will create a new instance and return the id number 80 * of the new instance. 81 * 82 * @param object $quiz the data that came from the form. 83 * @return mixed the id of the new instance on success, 84 * false or a string error message on failure. 85 */ 86 function quiz_add_instance($quiz) { 87 global $DB; 88 $cmid = $quiz->coursemodule; 89 90 // Process the options from the form. 91 $quiz->timecreated = time(); 92 $result = quiz_process_options($quiz); 93 if ($result && is_string($result)) { 94 return $result; 95 } 96 97 // Try to store it in the database. 98 $quiz->id = $DB->insert_record('quiz', $quiz); 99 100 // Create the first section for this quiz. 101 $DB->insert_record('quiz_sections', array('quizid' => $quiz->id, 102 'firstslot' => 1, 'heading' => '', 'shufflequestions' => 0)); 103 104 // Do the processing required after an add or an update. 105 quiz_after_add_or_update($quiz); 106 107 return $quiz->id; 108 } 109 110 /** 111 * Given an object containing all the necessary data, 112 * (defined by the form in mod_form.php) this function 113 * will update an existing instance with new data. 114 * 115 * @param object $quiz the data that came from the form. 116 * @return mixed true on success, false or a string error message on failure. 117 */ 118 function quiz_update_instance($quiz, $mform) { 119 global $CFG, $DB; 120 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 121 122 // Process the options from the form. 123 $result = quiz_process_options($quiz); 124 if ($result && is_string($result)) { 125 return $result; 126 } 127 128 // Get the current value, so we can see what changed. 129 $oldquiz = $DB->get_record('quiz', array('id' => $quiz->instance)); 130 131 // We need two values from the existing DB record that are not in the form, 132 // in some of the function calls below. 133 $quiz->sumgrades = $oldquiz->sumgrades; 134 $quiz->grade = $oldquiz->grade; 135 136 // Update the database. 137 $quiz->id = $quiz->instance; 138 $DB->update_record('quiz', $quiz); 139 140 // Do the processing required after an add or an update. 141 quiz_after_add_or_update($quiz); 142 143 if ($oldquiz->grademethod != $quiz->grademethod) { 144 quiz_update_all_final_grades($quiz); 145 quiz_update_grades($quiz); 146 } 147 148 $quizdateschanged = $oldquiz->timelimit != $quiz->timelimit 149 || $oldquiz->timeclose != $quiz->timeclose 150 || $oldquiz->graceperiod != $quiz->graceperiod; 151 if ($quizdateschanged) { 152 quiz_update_open_attempts(array('quizid' => $quiz->id)); 153 } 154 155 // Delete any previous preview attempts. 156 quiz_delete_previews($quiz); 157 158 // Repaginate, if asked to. 159 if (!empty($quiz->repaginatenow) && !quiz_has_attempts($quiz->id)) { 160 quiz_repaginate_questions($quiz->id, $quiz->questionsperpage); 161 } 162 163 return true; 164 } 165 166 /** 167 * Given an ID of an instance of this module, 168 * this function will permanently delete the instance 169 * and any data that depends on it. 170 * 171 * @param int $id the id of the quiz to delete. 172 * @return bool success or failure. 173 */ 174 function quiz_delete_instance($id) { 175 global $DB; 176 177 $quiz = $DB->get_record('quiz', array('id' => $id), '*', MUST_EXIST); 178 179 quiz_delete_all_attempts($quiz); 180 quiz_delete_all_overrides($quiz); 181 quiz_delete_references($quiz->id); 182 183 // We need to do the following deletes before we try and delete randoms, otherwise they would still be 'in use'. 184 $DB->delete_records('quiz_slots', array('quizid' => $quiz->id)); 185 $DB->delete_records('quiz_sections', array('quizid' => $quiz->id)); 186 187 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id)); 188 189 quiz_access_manager::delete_settings($quiz); 190 191 $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id)); 192 foreach ($events as $event) { 193 $event = calendar_event::load($event); 194 $event->delete(); 195 } 196 197 quiz_grade_item_delete($quiz); 198 // We must delete the module record after we delete the grade item. 199 $DB->delete_records('quiz', array('id' => $quiz->id)); 200 201 return true; 202 } 203 204 /** 205 * Deletes a quiz override from the database and clears any corresponding calendar events 206 * 207 * @param object $quiz The quiz object. 208 * @param int $overrideid The id of the override being deleted 209 * @param bool $log Whether to trigger logs. 210 * @return bool true on success 211 */ 212 function quiz_delete_override($quiz, $overrideid, $log = true) { 213 global $DB; 214 215 if (!isset($quiz->cmid)) { 216 $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course); 217 $quiz->cmid = $cm->id; 218 } 219 220 $override = $DB->get_record('quiz_overrides', array('id' => $overrideid), '*', MUST_EXIST); 221 222 // Delete the events. 223 if (isset($override->groupid)) { 224 // Create the search array for a group override. 225 $eventsearcharray = array('modulename' => 'quiz', 226 'instance' => $quiz->id, 'groupid' => (int)$override->groupid); 227 $cachekey = "{$quiz->id}_g_{$override->groupid}"; 228 } else { 229 // Create the search array for a user override. 230 $eventsearcharray = array('modulename' => 'quiz', 231 'instance' => $quiz->id, 'userid' => (int)$override->userid); 232 $cachekey = "{$quiz->id}_u_{$override->userid}"; 233 } 234 $events = $DB->get_records('event', $eventsearcharray); 235 foreach ($events as $event) { 236 $eventold = calendar_event::load($event); 237 $eventold->delete(); 238 } 239 240 $DB->delete_records('quiz_overrides', array('id' => $overrideid)); 241 cache::make('mod_quiz', 'overrides')->delete($cachekey); 242 243 if ($log) { 244 // Set the common parameters for one of the events we will be triggering. 245 $params = array( 246 'objectid' => $override->id, 247 'context' => context_module::instance($quiz->cmid), 248 'other' => array( 249 'quizid' => $override->quiz 250 ) 251 ); 252 // Determine which override deleted event to fire. 253 if (!empty($override->userid)) { 254 $params['relateduserid'] = $override->userid; 255 $event = \mod_quiz\event\user_override_deleted::create($params); 256 } else { 257 $params['other']['groupid'] = $override->groupid; 258 $event = \mod_quiz\event\group_override_deleted::create($params); 259 } 260 261 // Trigger the override deleted event. 262 $event->add_record_snapshot('quiz_overrides', $override); 263 $event->trigger(); 264 } 265 266 return true; 267 } 268 269 /** 270 * Deletes all quiz overrides from the database and clears any corresponding calendar events 271 * 272 * @param object $quiz The quiz object. 273 * @param bool $log Whether to trigger logs. 274 */ 275 function quiz_delete_all_overrides($quiz, $log = true) { 276 global $DB; 277 278 $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id'); 279 foreach ($overrides as $override) { 280 quiz_delete_override($quiz, $override->id, $log); 281 } 282 } 283 284 /** 285 * Updates a quiz object with override information for a user. 286 * 287 * Algorithm: For each quiz setting, if there is a matching user-specific override, 288 * then use that otherwise, if there are group-specific overrides, return the most 289 * lenient combination of them. If neither applies, leave the quiz setting unchanged. 290 * 291 * Special case: if there is more than one password that applies to the user, then 292 * quiz->extrapasswords will contain an array of strings giving the remaining 293 * passwords. 294 * 295 * @param object $quiz The quiz object. 296 * @param int $userid The userid. 297 * @return object $quiz The updated quiz object. 298 */ 299 function quiz_update_effective_access($quiz, $userid) { 300 global $DB; 301 302 // Check for user override. 303 $override = $DB->get_record('quiz_overrides', array('quiz' => $quiz->id, 'userid' => $userid)); 304 305 if (!$override) { 306 $override = new stdClass(); 307 $override->timeopen = null; 308 $override->timeclose = null; 309 $override->timelimit = null; 310 $override->attempts = null; 311 $override->password = null; 312 } 313 314 // Check for group overrides. 315 $groupings = groups_get_user_groups($quiz->course, $userid); 316 317 if (!empty($groupings[0])) { 318 // Select all overrides that apply to the User's groups. 319 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0])); 320 $sql = "SELECT * FROM {quiz_overrides} 321 WHERE groupid $extra AND quiz = ?"; 322 $params[] = $quiz->id; 323 $records = $DB->get_records_sql($sql, $params); 324 325 // Combine the overrides. 326 $opens = array(); 327 $closes = array(); 328 $limits = array(); 329 $attempts = array(); 330 $passwords = array(); 331 332 foreach ($records as $gpoverride) { 333 if (isset($gpoverride->timeopen)) { 334 $opens[] = $gpoverride->timeopen; 335 } 336 if (isset($gpoverride->timeclose)) { 337 $closes[] = $gpoverride->timeclose; 338 } 339 if (isset($gpoverride->timelimit)) { 340 $limits[] = $gpoverride->timelimit; 341 } 342 if (isset($gpoverride->attempts)) { 343 $attempts[] = $gpoverride->attempts; 344 } 345 if (isset($gpoverride->password)) { 346 $passwords[] = $gpoverride->password; 347 } 348 } 349 // If there is a user override for a setting, ignore the group override. 350 if (is_null($override->timeopen) && count($opens)) { 351 $override->timeopen = min($opens); 352 } 353 if (is_null($override->timeclose) && count($closes)) { 354 if (in_array(0, $closes)) { 355 $override->timeclose = 0; 356 } else { 357 $override->timeclose = max($closes); 358 } 359 } 360 if (is_null($override->timelimit) && count($limits)) { 361 if (in_array(0, $limits)) { 362 $override->timelimit = 0; 363 } else { 364 $override->timelimit = max($limits); 365 } 366 } 367 if (is_null($override->attempts) && count($attempts)) { 368 if (in_array(0, $attempts)) { 369 $override->attempts = 0; 370 } else { 371 $override->attempts = max($attempts); 372 } 373 } 374 if (is_null($override->password) && count($passwords)) { 375 $override->password = array_shift($passwords); 376 if (count($passwords)) { 377 $override->extrapasswords = $passwords; 378 } 379 } 380 381 } 382 383 // Merge with quiz defaults. 384 $keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password', 'extrapasswords'); 385 foreach ($keys as $key) { 386 if (isset($override->{$key})) { 387 $quiz->{$key} = $override->{$key}; 388 } 389 } 390 391 return $quiz; 392 } 393 394 /** 395 * Delete all the attempts belonging to a quiz. 396 * 397 * @param object $quiz The quiz object. 398 */ 399 function quiz_delete_all_attempts($quiz) { 400 global $CFG, $DB; 401 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 402 question_engine::delete_questions_usage_by_activities(new qubaids_for_quiz($quiz->id)); 403 $DB->delete_records('quiz_attempts', array('quiz' => $quiz->id)); 404 $DB->delete_records('quiz_grades', array('quiz' => $quiz->id)); 405 } 406 407 /** 408 * Delete all the attempts belonging to a user in a particular quiz. 409 * 410 * @param object $quiz The quiz object. 411 * @param object $user The user object. 412 */ 413 function quiz_delete_user_attempts($quiz, $user) { 414 global $CFG, $DB; 415 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 416 question_engine::delete_questions_usage_by_activities(new qubaids_for_quiz_user($quiz->get_quizid(), $user->id)); 417 $params = [ 418 'quiz' => $quiz->get_quizid(), 419 'userid' => $user->id, 420 ]; 421 $DB->delete_records('quiz_attempts', $params); 422 $DB->delete_records('quiz_grades', $params); 423 } 424 425 /** 426 * Get the best current grade for a particular user in a quiz. 427 * 428 * @param object $quiz the quiz settings. 429 * @param int $userid the id of the user. 430 * @return float the user's current grade for this quiz, or null if this user does 431 * not have a grade on this quiz. 432 */ 433 function quiz_get_best_grade($quiz, $userid) { 434 global $DB; 435 $grade = $DB->get_field('quiz_grades', 'grade', 436 array('quiz' => $quiz->id, 'userid' => $userid)); 437 438 // Need to detect errors/no result, without catching 0 grades. 439 if ($grade === false) { 440 return null; 441 } 442 443 return $grade + 0; // Convert to number. 444 } 445 446 /** 447 * Is this a graded quiz? If this method returns true, you can assume that 448 * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to 449 * divide by them). 450 * 451 * @param object $quiz a row from the quiz table. 452 * @return bool whether this is a graded quiz. 453 */ 454 function quiz_has_grades($quiz) { 455 return $quiz->grade >= 0.000005 && $quiz->sumgrades >= 0.000005; 456 } 457 458 /** 459 * Does this quiz allow multiple tries? 460 * 461 * @return bool 462 */ 463 function quiz_allows_multiple_tries($quiz) { 464 $bt = question_engine::get_behaviour_type($quiz->preferredbehaviour); 465 return $bt->allows_multiple_submitted_responses(); 466 } 467 468 /** 469 * Return a small object with summary information about what a 470 * user has done with a given particular instance of this module 471 * Used for user activity reports. 472 * $return->time = the time they did it 473 * $return->info = a short text description 474 * 475 * @param object $course 476 * @param object $user 477 * @param object $mod 478 * @param object $quiz 479 * @return object|null 480 */ 481 function quiz_user_outline($course, $user, $mod, $quiz) { 482 global $DB, $CFG; 483 require_once($CFG->libdir . '/gradelib.php'); 484 $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id); 485 486 if (empty($grades->items[0]->grades)) { 487 return null; 488 } else { 489 $grade = reset($grades->items[0]->grades); 490 } 491 492 $result = new stdClass(); 493 // If the user can't see hidden grades, don't return that information. 494 $gitem = grade_item::fetch(array('id' => $grades->items[0]->id)); 495 if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { 496 $result->info = get_string('gradenoun') . ': ' . $grade->str_long_grade; 497 } else { 498 $result->info = get_string('gradenoun') . ': ' . get_string('hidden', 'grades'); 499 } 500 501 $result->time = grade_get_date_for_user_grade($grade, $user); 502 503 return $result; 504 } 505 506 /** 507 * Print a detailed representation of what a user has done with 508 * a given particular instance of this module, for user activity reports. 509 * 510 * @param object $course 511 * @param object $user 512 * @param object $mod 513 * @param object $quiz 514 * @return bool 515 */ 516 function quiz_user_complete($course, $user, $mod, $quiz) { 517 global $DB, $CFG, $OUTPUT; 518 require_once($CFG->libdir . '/gradelib.php'); 519 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 520 521 $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id); 522 if (!empty($grades->items[0]->grades)) { 523 $grade = reset($grades->items[0]->grades); 524 // If the user can't see hidden grades, don't return that information. 525 $gitem = grade_item::fetch(array('id' => $grades->items[0]->id)); 526 if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { 527 echo $OUTPUT->container(get_string('gradenoun').': '.$grade->str_long_grade); 528 if ($grade->str_feedback) { 529 echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback); 530 } 531 } else { 532 echo $OUTPUT->container(get_string('gradenoun') . ': ' . get_string('hidden', 'grades')); 533 if ($grade->str_feedback) { 534 echo $OUTPUT->container(get_string('feedback').': '.get_string('hidden', 'grades')); 535 } 536 } 537 } 538 539 if ($attempts = $DB->get_records('quiz_attempts', 540 array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) { 541 foreach ($attempts as $attempt) { 542 echo get_string('attempt', 'quiz', $attempt->attempt) . ': '; 543 if ($attempt->state != quiz_attempt::FINISHED) { 544 echo quiz_attempt_state_name($attempt->state); 545 } else { 546 if (!isset($gitem)) { 547 if (!empty($grades->items[0]->grades)) { 548 $gitem = grade_item::fetch(array('id' => $grades->items[0]->id)); 549 } else { 550 $gitem = new stdClass(); 551 $gitem->hidden = true; 552 } 553 } 554 if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { 555 echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' . quiz_format_grade($quiz, $quiz->sumgrades); 556 } else { 557 echo get_string('hidden', 'grades'); 558 } 559 echo ' - '.userdate($attempt->timefinish).'<br />'; 560 } 561 } 562 } else { 563 print_string('noattempts', 'quiz'); 564 } 565 566 return true; 567 } 568 569 570 /** 571 * @param int|array $quizids A quiz ID, or an array of quiz IDs. 572 * @param int $userid the userid. 573 * @param string $status 'all', 'finished' or 'unfinished' to control 574 * @param bool $includepreviews 575 * @return array of all the user's attempts at this quiz. Returns an empty 576 * array if there are none. 577 */ 578 function quiz_get_user_attempts($quizids, $userid, $status = 'finished', $includepreviews = false) { 579 global $DB, $CFG; 580 // TODO MDL-33071 it is very annoying to have to included all of locallib.php 581 // just to get the quiz_attempt::FINISHED constants, but I will try to sort 582 // that out properly for Moodle 2.4. For now, I will just do a quick fix for 583 // MDL-33048. 584 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 585 586 $params = array(); 587 switch ($status) { 588 case 'all': 589 $statuscondition = ''; 590 break; 591 592 case 'finished': 593 $statuscondition = ' AND state IN (:state1, :state2)'; 594 $params['state1'] = quiz_attempt::FINISHED; 595 $params['state2'] = quiz_attempt::ABANDONED; 596 break; 597 598 case 'unfinished': 599 $statuscondition = ' AND state IN (:state1, :state2)'; 600 $params['state1'] = quiz_attempt::IN_PROGRESS; 601 $params['state2'] = quiz_attempt::OVERDUE; 602 break; 603 } 604 605 $quizids = (array) $quizids; 606 list($insql, $inparams) = $DB->get_in_or_equal($quizids, SQL_PARAMS_NAMED); 607 $params += $inparams; 608 $params['userid'] = $userid; 609 610 $previewclause = ''; 611 if (!$includepreviews) { 612 $previewclause = ' AND preview = 0'; 613 } 614 615 return $DB->get_records_select('quiz_attempts', 616 "quiz $insql AND userid = :userid" . $previewclause . $statuscondition, 617 $params, 'quiz, attempt ASC'); 618 } 619 620 /** 621 * Return grade for given user or all users. 622 * 623 * @param int $quizid id of quiz 624 * @param int $userid optional user id, 0 means all users 625 * @return array array of grades, false if none. These are raw grades. They should 626 * be processed with quiz_format_grade for display. 627 */ 628 function quiz_get_user_grades($quiz, $userid = 0) { 629 global $CFG, $DB; 630 631 $params = array($quiz->id); 632 $usertest = ''; 633 if ($userid) { 634 $params[] = $userid; 635 $usertest = 'AND u.id = ?'; 636 } 637 return $DB->get_records_sql(" 638 SELECT 639 u.id, 640 u.id AS userid, 641 qg.grade AS rawgrade, 642 qg.timemodified AS dategraded, 643 MAX(qa.timefinish) AS datesubmitted 644 645 FROM {user} u 646 JOIN {quiz_grades} qg ON u.id = qg.userid 647 JOIN {quiz_attempts} qa ON qa.quiz = qg.quiz AND qa.userid = u.id 648 649 WHERE qg.quiz = ? 650 $usertest 651 GROUP BY u.id, qg.grade, qg.timemodified", $params); 652 } 653 654 /** 655 * Round a grade to to the correct number of decimal places, and format it for display. 656 * 657 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used. 658 * @param float $grade The grade to round. 659 * @return float 660 */ 661 function quiz_format_grade($quiz, $grade) { 662 if (is_null($grade)) { 663 return get_string('notyetgraded', 'quiz'); 664 } 665 return format_float($grade, $quiz->decimalpoints); 666 } 667 668 /** 669 * Determine the correct number of decimal places required to format a grade. 670 * 671 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used. 672 * @return integer 673 */ 674 function quiz_get_grade_format($quiz) { 675 if (empty($quiz->questiondecimalpoints)) { 676 $quiz->questiondecimalpoints = -1; 677 } 678 679 if ($quiz->questiondecimalpoints == -1) { 680 return $quiz->decimalpoints; 681 } 682 683 return $quiz->questiondecimalpoints; 684 } 685 686 /** 687 * Round a grade to the correct number of decimal places, and format it for display. 688 * 689 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used. 690 * @param float $grade The grade to round. 691 * @return float 692 */ 693 function quiz_format_question_grade($quiz, $grade) { 694 return format_float($grade, quiz_get_grade_format($quiz)); 695 } 696 697 /** 698 * Update grades in central gradebook 699 * 700 * @category grade 701 * @param object $quiz the quiz settings. 702 * @param int $userid specific user only, 0 means all users. 703 * @param bool $nullifnone If a single user is specified and $nullifnone is true a grade item with a null rawgrade will be inserted 704 */ 705 function quiz_update_grades($quiz, $userid = 0, $nullifnone = true) { 706 global $CFG, $DB; 707 require_once($CFG->libdir . '/gradelib.php'); 708 709 if ($quiz->grade == 0) { 710 quiz_grade_item_update($quiz); 711 712 } else if ($grades = quiz_get_user_grades($quiz, $userid)) { 713 quiz_grade_item_update($quiz, $grades); 714 715 } else if ($userid && $nullifnone) { 716 $grade = new stdClass(); 717 $grade->userid = $userid; 718 $grade->rawgrade = null; 719 quiz_grade_item_update($quiz, $grade); 720 721 } else { 722 quiz_grade_item_update($quiz); 723 } 724 } 725 726 /** 727 * Create or update the grade item for given quiz 728 * 729 * @category grade 730 * @param object $quiz object with extra cmidnumber 731 * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook 732 * @return int 0 if ok, error code otherwise 733 */ 734 function quiz_grade_item_update($quiz, $grades = null) { 735 global $CFG, $OUTPUT; 736 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 737 require_once($CFG->libdir . '/gradelib.php'); 738 739 if (property_exists($quiz, 'cmidnumber')) { // May not be always present. 740 $params = array('itemname' => $quiz->name, 'idnumber' => $quiz->cmidnumber); 741 } else { 742 $params = array('itemname' => $quiz->name); 743 } 744 745 if ($quiz->grade > 0) { 746 $params['gradetype'] = GRADE_TYPE_VALUE; 747 $params['grademax'] = $quiz->grade; 748 $params['grademin'] = 0; 749 750 } else { 751 $params['gradetype'] = GRADE_TYPE_NONE; 752 } 753 754 // What this is trying to do: 755 // 1. If the quiz is set to not show grades while the quiz is still open, 756 // and is set to show grades after the quiz is closed, then create the 757 // grade_item with a show-after date that is the quiz close date. 758 // 2. If the quiz is set to not show grades at either of those times, 759 // create the grade_item as hidden. 760 // 3. If the quiz is set to show grades, create the grade_item visible. 761 $openreviewoptions = mod_quiz_display_options::make_from_quiz($quiz, 762 mod_quiz_display_options::LATER_WHILE_OPEN); 763 $closedreviewoptions = mod_quiz_display_options::make_from_quiz($quiz, 764 mod_quiz_display_options::AFTER_CLOSE); 765 if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX && 766 $closedreviewoptions->marks < question_display_options::MARK_AND_MAX) { 767 $params['hidden'] = 1; 768 769 } else if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX && 770 $closedreviewoptions->marks >= question_display_options::MARK_AND_MAX) { 771 if ($quiz->timeclose) { 772 $params['hidden'] = $quiz->timeclose; 773 } else { 774 $params['hidden'] = 1; 775 } 776 777 } else { 778 // Either 779 // a) both open and closed enabled 780 // b) open enabled, closed disabled - we can not "hide after", 781 // grades are kept visible even after closing. 782 $params['hidden'] = 0; 783 } 784 785 if (!$params['hidden']) { 786 // If the grade item is not hidden by the quiz logic, then we need to 787 // hide it if the quiz is hidden from students. 788 if (property_exists($quiz, 'visible')) { 789 // Saving the quiz form, and cm not yet updated in the database. 790 $params['hidden'] = !$quiz->visible; 791 } else { 792 $cm = get_coursemodule_from_instance('quiz', $quiz->id); 793 $params['hidden'] = !$cm->visible; 794 } 795 } 796 797 if ($grades === 'reset') { 798 $params['reset'] = true; 799 $grades = null; 800 } 801 802 $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id); 803 if (!empty($gradebook_grades->items)) { 804 $grade_item = $gradebook_grades->items[0]; 805 if ($grade_item->locked) { 806 // NOTE: this is an extremely nasty hack! It is not a bug if this confirmation fails badly. --skodak. 807 $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT); 808 if (!$confirm_regrade) { 809 if (!AJAX_SCRIPT) { 810 $message = get_string('gradeitemislocked', 'grades'); 811 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id . 812 '&mode=overview'; 813 $regrade_link = qualified_me() . '&confirm_regrade=1'; 814 echo $OUTPUT->box_start('generalbox', 'notice'); 815 echo '<p>'. $message .'</p>'; 816 echo $OUTPUT->container_start('buttons'); 817 echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades')); 818 echo $OUTPUT->single_button($back_link, get_string('cancel')); 819 echo $OUTPUT->container_end(); 820 echo $OUTPUT->box_end(); 821 } 822 return GRADE_UPDATE_ITEM_LOCKED; 823 } 824 } 825 } 826 827 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params); 828 } 829 830 /** 831 * Delete grade item for given quiz 832 * 833 * @category grade 834 * @param object $quiz object 835 * @return object quiz 836 */ 837 function quiz_grade_item_delete($quiz) { 838 global $CFG; 839 require_once($CFG->libdir . '/gradelib.php'); 840 841 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, 842 null, array('deleted' => 1)); 843 } 844 845 /** 846 * This standard function will check all instances of this module 847 * and make sure there are up-to-date events created for each of them. 848 * If courseid = 0, then every quiz event in the site is checked, else 849 * only quiz events belonging to the course specified are checked. 850 * This function is used, in its new format, by restore_refresh_events() 851 * 852 * @param int $courseid 853 * @param int|stdClass $instance Quiz module instance or ID. 854 * @param int|stdClass $cm Course module object or ID (not used in this module). 855 * @return bool 856 */ 857 function quiz_refresh_events($courseid = 0, $instance = null, $cm = null) { 858 global $DB; 859 860 // If we have instance information then we can just update the one event instead of updating all events. 861 if (isset($instance)) { 862 if (!is_object($instance)) { 863 $instance = $DB->get_record('quiz', array('id' => $instance), '*', MUST_EXIST); 864 } 865 quiz_update_events($instance); 866 return true; 867 } 868 869 if ($courseid == 0) { 870 if (!$quizzes = $DB->get_records('quiz')) { 871 return true; 872 } 873 } else { 874 if (!$quizzes = $DB->get_records('quiz', array('course' => $courseid))) { 875 return true; 876 } 877 } 878 879 foreach ($quizzes as $quiz) { 880 quiz_update_events($quiz); 881 } 882 883 return true; 884 } 885 886 /** 887 * Returns all quiz graded users since a given time for specified quiz 888 */ 889 function quiz_get_recent_mod_activity(&$activities, &$index, $timestart, 890 $courseid, $cmid, $userid = 0, $groupid = 0) { 891 global $CFG, $USER, $DB; 892 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 893 894 $course = get_course($courseid); 895 $modinfo = get_fast_modinfo($course); 896 897 $cm = $modinfo->cms[$cmid]; 898 $quiz = $DB->get_record('quiz', array('id' => $cm->instance)); 899 900 if ($userid) { 901 $userselect = "AND u.id = :userid"; 902 $params['userid'] = $userid; 903 } else { 904 $userselect = ''; 905 } 906 907 if ($groupid) { 908 $groupselect = 'AND gm.groupid = :groupid'; 909 $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id'; 910 $params['groupid'] = $groupid; 911 } else { 912 $groupselect = ''; 913 $groupjoin = ''; 914 } 915 916 $params['timestart'] = $timestart; 917 $params['quizid'] = $quiz->id; 918 919 $userfieldsapi = \core_user\fields::for_userpic(); 920 $ufields = $userfieldsapi->get_sql('u', false, '', 'useridagain', false)->selects; 921 if (!$attempts = $DB->get_records_sql(" 922 SELECT qa.*, 923 {$ufields} 924 FROM {quiz_attempts} qa 925 JOIN {user} u ON u.id = qa.userid 926 $groupjoin 927 WHERE qa.timefinish > :timestart 928 AND qa.quiz = :quizid 929 AND qa.preview = 0 930 $userselect 931 $groupselect 932 ORDER BY qa.timefinish ASC", $params)) { 933 return; 934 } 935 936 $context = context_module::instance($cm->id); 937 $accessallgroups = has_capability('moodle/site:accessallgroups', $context); 938 $viewfullnames = has_capability('moodle/site:viewfullnames', $context); 939 $grader = has_capability('mod/quiz:viewreports', $context); 940 $groupmode = groups_get_activity_groupmode($cm, $course); 941 942 $usersgroups = null; 943 $aname = format_string($cm->name, true); 944 foreach ($attempts as $attempt) { 945 if ($attempt->userid != $USER->id) { 946 if (!$grader) { 947 // Grade permission required. 948 continue; 949 } 950 951 if ($groupmode == SEPARATEGROUPS and !$accessallgroups) { 952 $usersgroups = groups_get_all_groups($course->id, 953 $attempt->userid, $cm->groupingid); 954 $usersgroups = array_keys($usersgroups); 955 if (!array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid))) { 956 continue; 957 } 958 } 959 } 960 961 $options = quiz_get_review_options($quiz, $attempt, $context); 962 963 $tmpactivity = new stdClass(); 964 965 $tmpactivity->type = 'quiz'; 966 $tmpactivity->cmid = $cm->id; 967 $tmpactivity->name = $aname; 968 $tmpactivity->sectionnum = $cm->sectionnum; 969 $tmpactivity->timestamp = $attempt->timefinish; 970 971 $tmpactivity->content = new stdClass(); 972 $tmpactivity->content->attemptid = $attempt->id; 973 $tmpactivity->content->attempt = $attempt->attempt; 974 if (quiz_has_grades($quiz) && $options->marks >= question_display_options::MARK_AND_MAX) { 975 $tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades); 976 $tmpactivity->content->maxgrade = quiz_format_grade($quiz, $quiz->sumgrades); 977 } else { 978 $tmpactivity->content->sumgrades = null; 979 $tmpactivity->content->maxgrade = null; 980 } 981 982 $tmpactivity->user = user_picture::unalias($attempt, null, 'useridagain'); 983 $tmpactivity->user->fullname = fullname($tmpactivity->user, $viewfullnames); 984 985 $activities[$index++] = $tmpactivity; 986 } 987 } 988 989 function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) { 990 global $CFG, $OUTPUT; 991 992 echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">'; 993 994 echo '<tr><td class="userpicture" valign="top">'; 995 echo $OUTPUT->user_picture($activity->user, array('courseid' => $courseid)); 996 echo '</td><td>'; 997 998 if ($detail) { 999 $modname = $modnames[$activity->type]; 1000 echo '<div class="title">'; 1001 echo $OUTPUT->image_icon('monologo', $modname, $activity->type); 1002 echo '<a href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' . 1003 $activity->cmid . '">' . $activity->name . '</a>'; 1004 echo '</div>'; 1005 } 1006 1007 echo '<div class="grade">'; 1008 echo get_string('attempt', 'quiz', $activity->content->attempt); 1009 if (isset($activity->content->maxgrade)) { 1010 $grades = $activity->content->sumgrades . ' / ' . $activity->content->maxgrade; 1011 echo ': (<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . 1012 $activity->content->attemptid . '">' . $grades . '</a>)'; 1013 } 1014 echo '</div>'; 1015 1016 echo '<div class="user">'; 1017 echo '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $activity->user->id . 1018 '&course=' . $courseid . '">' . $activity->user->fullname . 1019 '</a> - ' . userdate($activity->timestamp); 1020 echo '</div>'; 1021 1022 echo '</td></tr></table>'; 1023 1024 return; 1025 } 1026 1027 /** 1028 * Pre-process the quiz options form data, making any necessary adjustments. 1029 * Called by add/update instance in this file. 1030 * 1031 * @param object $quiz The variables set on the form. 1032 */ 1033 function quiz_process_options($quiz) { 1034 global $CFG; 1035 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 1036 require_once($CFG->libdir . '/questionlib.php'); 1037 1038 $quiz->timemodified = time(); 1039 1040 // Quiz name. 1041 if (!empty($quiz->name)) { 1042 $quiz->name = trim($quiz->name); 1043 } 1044 1045 // Password field - different in form to stop browsers that remember passwords 1046 // getting confused. 1047 $quiz->password = $quiz->quizpassword; 1048 unset($quiz->quizpassword); 1049 1050 // Quiz feedback. 1051 if (isset($quiz->feedbacktext)) { 1052 // Clean up the boundary text. 1053 for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) { 1054 if (empty($quiz->feedbacktext[$i]['text'])) { 1055 $quiz->feedbacktext[$i]['text'] = ''; 1056 } else { 1057 $quiz->feedbacktext[$i]['text'] = trim($quiz->feedbacktext[$i]['text']); 1058 } 1059 } 1060 1061 // Check the boundary value is a number or a percentage, and in range. 1062 $i = 0; 1063 while (!empty($quiz->feedbackboundaries[$i])) { 1064 $boundary = trim($quiz->feedbackboundaries[$i]); 1065 if (!is_numeric($boundary)) { 1066 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') { 1067 $boundary = trim(substr($boundary, 0, -1)); 1068 if (is_numeric($boundary)) { 1069 $boundary = $boundary * $quiz->grade / 100.0; 1070 } else { 1071 return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1); 1072 } 1073 } 1074 } 1075 if ($boundary <= 0 || $boundary >= $quiz->grade) { 1076 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1); 1077 } 1078 if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) { 1079 return get_string('feedbackerrororder', 'quiz', $i + 1); 1080 } 1081 $quiz->feedbackboundaries[$i] = $boundary; 1082 $i += 1; 1083 } 1084 $numboundaries = $i; 1085 1086 // Check there is nothing in the remaining unused fields. 1087 if (!empty($quiz->feedbackboundaries)) { 1088 for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) { 1089 if (!empty($quiz->feedbackboundaries[$i]) && 1090 trim($quiz->feedbackboundaries[$i]) != '') { 1091 return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1); 1092 } 1093 } 1094 } 1095 for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) { 1096 if (!empty($quiz->feedbacktext[$i]['text']) && 1097 trim($quiz->feedbacktext[$i]['text']) != '') { 1098 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1); 1099 } 1100 } 1101 // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade(). 1102 $quiz->feedbackboundaries[-1] = $quiz->grade + 1; 1103 $quiz->feedbackboundaries[$numboundaries] = 0; 1104 $quiz->feedbackboundarycount = $numboundaries; 1105 } else { 1106 $quiz->feedbackboundarycount = -1; 1107 } 1108 1109 // Combing the individual settings into the review columns. 1110 $quiz->reviewattempt = quiz_review_option_form_to_db($quiz, 'attempt'); 1111 $quiz->reviewcorrectness = quiz_review_option_form_to_db($quiz, 'correctness'); 1112 $quiz->reviewmarks = quiz_review_option_form_to_db($quiz, 'marks'); 1113 $quiz->reviewspecificfeedback = quiz_review_option_form_to_db($quiz, 'specificfeedback'); 1114 $quiz->reviewgeneralfeedback = quiz_review_option_form_to_db($quiz, 'generalfeedback'); 1115 $quiz->reviewrightanswer = quiz_review_option_form_to_db($quiz, 'rightanswer'); 1116 $quiz->reviewoverallfeedback = quiz_review_option_form_to_db($quiz, 'overallfeedback'); 1117 $quiz->reviewattempt |= mod_quiz_display_options::DURING; 1118 $quiz->reviewoverallfeedback &= ~mod_quiz_display_options::DURING; 1119 1120 // Ensure that disabled checkboxes in completion settings are set to 0. 1121 // But only if the completion settinsg are unlocked. 1122 if (!empty($quiz->completionunlocked)) { 1123 if (empty($quiz->completionusegrade)) { 1124 $quiz->completionpassgrade = 0; 1125 } 1126 if (empty($quiz->completionpassgrade)) { 1127 $quiz->completionattemptsexhausted = 0; 1128 } 1129 if (empty($quiz->completionminattemptsenabled)) { 1130 $quiz->completionminattempts = 0; 1131 } 1132 } 1133 } 1134 1135 /** 1136 * Helper function for {@link quiz_process_options()}. 1137 * @param object $fromform the sumbitted form date. 1138 * @param string $field one of the review option field names. 1139 */ 1140 function quiz_review_option_form_to_db($fromform, $field) { 1141 static $times = array( 1142 'during' => mod_quiz_display_options::DURING, 1143 'immediately' => mod_quiz_display_options::IMMEDIATELY_AFTER, 1144 'open' => mod_quiz_display_options::LATER_WHILE_OPEN, 1145 'closed' => mod_quiz_display_options::AFTER_CLOSE, 1146 ); 1147 1148 $review = 0; 1149 foreach ($times as $whenname => $when) { 1150 $fieldname = $field . $whenname; 1151 if (!empty($fromform->$fieldname)) { 1152 $review |= $when; 1153 unset($fromform->$fieldname); 1154 } 1155 } 1156 1157 return $review; 1158 } 1159 1160 /** 1161 * This function is called at the end of quiz_add_instance 1162 * and quiz_update_instance, to do the common processing. 1163 * 1164 * @param object $quiz the quiz object. 1165 */ 1166 function quiz_after_add_or_update($quiz) { 1167 global $DB; 1168 $cmid = $quiz->coursemodule; 1169 1170 // We need to use context now, so we need to make sure all needed info is already in db. 1171 $DB->set_field('course_modules', 'instance', $quiz->id, array('id'=>$cmid)); 1172 $context = context_module::instance($cmid); 1173 1174 // Save the feedback. 1175 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id)); 1176 1177 for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) { 1178 $feedback = new stdClass(); 1179 $feedback->quizid = $quiz->id; 1180 $feedback->feedbacktext = $quiz->feedbacktext[$i]['text']; 1181 $feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format']; 1182 $feedback->mingrade = $quiz->feedbackboundaries[$i]; 1183 $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1]; 1184 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1185 $feedbacktext = file_save_draft_area_files((int)$quiz->feedbacktext[$i]['itemid'], 1186 $context->id, 'mod_quiz', 'feedback', $feedback->id, 1187 array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0), 1188 $quiz->feedbacktext[$i]['text']); 1189 $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext, 1190 array('id' => $feedback->id)); 1191 } 1192 1193 // Store any settings belonging to the access rules. 1194 quiz_access_manager::save_settings($quiz); 1195 1196 // Update the events relating to this quiz. 1197 quiz_update_events($quiz); 1198 $completionexpected = (!empty($quiz->completionexpected)) ? $quiz->completionexpected : null; 1199 \core_completion\api::update_completion_date_event($quiz->coursemodule, 'quiz', $quiz->id, $completionexpected); 1200 1201 // Update related grade item. 1202 quiz_grade_item_update($quiz); 1203 } 1204 1205 /** 1206 * This function updates the events associated to the quiz. 1207 * If $override is non-zero, then it updates only the events 1208 * associated with the specified override. 1209 * 1210 * @uses QUIZ_MAX_EVENT_LENGTH 1211 * @param object $quiz the quiz object. 1212 * @param object optional $override limit to a specific override 1213 */ 1214 function quiz_update_events($quiz, $override = null) { 1215 global $DB; 1216 1217 // Load the old events relating to this quiz. 1218 $conds = array('modulename'=>'quiz', 1219 'instance'=>$quiz->id); 1220 if (!empty($override)) { 1221 // Only load events for this override. 1222 if (isset($override->userid)) { 1223 $conds['userid'] = $override->userid; 1224 } else { 1225 $conds['groupid'] = $override->groupid; 1226 } 1227 } 1228 $oldevents = $DB->get_records('event', $conds, 'id ASC'); 1229 1230 // Now make a to-do list of all that needs to be updated. 1231 if (empty($override)) { 1232 // We are updating the primary settings for the quiz, so we need to add all the overrides. 1233 $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id ASC'); 1234 // It is necessary to add an empty stdClass to the beginning of the array as the $oldevents 1235 // list contains the original (non-override) event for the module. If this is not included 1236 // the logic below will end up updating the wrong row when we try to reconcile this $overrides 1237 // list against the $oldevents list. 1238 array_unshift($overrides, new stdClass()); 1239 } else { 1240 // Just do the one override. 1241 $overrides = array($override); 1242 } 1243 1244 // Get group override priorities. 1245 $grouppriorities = quiz_get_group_override_priorities($quiz->id); 1246 1247 foreach ($overrides as $current) { 1248 $groupid = isset($current->groupid)? $current->groupid : 0; 1249 $userid = isset($current->userid)? $current->userid : 0; 1250 $timeopen = isset($current->timeopen)? $current->timeopen : $quiz->timeopen; 1251 $timeclose = isset($current->timeclose)? $current->timeclose : $quiz->timeclose; 1252 1253 // Only add open/close events for an override if they differ from the quiz default. 1254 $addopen = empty($current->id) || !empty($current->timeopen); 1255 $addclose = empty($current->id) || !empty($current->timeclose); 1256 1257 if (!empty($quiz->coursemodule)) { 1258 $cmid = $quiz->coursemodule; 1259 } else { 1260 $cmid = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course)->id; 1261 } 1262 1263 $event = new stdClass(); 1264 $event->type = !$timeclose ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; 1265 $event->description = format_module_intro('quiz', $quiz, $cmid, false); 1266 $event->format = FORMAT_HTML; 1267 // Events module won't show user events when the courseid is nonzero. 1268 $event->courseid = ($userid) ? 0 : $quiz->course; 1269 $event->groupid = $groupid; 1270 $event->userid = $userid; 1271 $event->modulename = 'quiz'; 1272 $event->instance = $quiz->id; 1273 $event->timestart = $timeopen; 1274 $event->timeduration = max($timeclose - $timeopen, 0); 1275 $event->timesort = $timeopen; 1276 $event->visible = instance_is_visible('quiz', $quiz); 1277 $event->eventtype = QUIZ_EVENT_TYPE_OPEN; 1278 $event->priority = null; 1279 1280 // Determine the event name and priority. 1281 if ($groupid) { 1282 // Group override event. 1283 $params = new stdClass(); 1284 $params->quiz = $quiz->name; 1285 $params->group = groups_get_group_name($groupid); 1286 if ($params->group === false) { 1287 // Group doesn't exist, just skip it. 1288 continue; 1289 } 1290 $eventname = get_string('overridegroupeventname', 'quiz', $params); 1291 // Set group override priority. 1292 if ($grouppriorities !== null) { 1293 $openpriorities = $grouppriorities['open']; 1294 if (isset($openpriorities[$timeopen])) { 1295 $event->priority = $openpriorities[$timeopen]; 1296 } 1297 } 1298 } else if ($userid) { 1299 // User override event. 1300 $params = new stdClass(); 1301 $params->quiz = $quiz->name; 1302 $eventname = get_string('overrideusereventname', 'quiz', $params); 1303 // Set user override priority. 1304 $event->priority = CALENDAR_EVENT_USER_OVERRIDE_PRIORITY; 1305 } else { 1306 // The parent event. 1307 $eventname = $quiz->name; 1308 } 1309 1310 if ($addopen or $addclose) { 1311 // Separate start and end events. 1312 $event->timeduration = 0; 1313 if ($timeopen && $addopen) { 1314 if ($oldevent = array_shift($oldevents)) { 1315 $event->id = $oldevent->id; 1316 } else { 1317 unset($event->id); 1318 } 1319 $event->name = get_string('quizeventopens', 'quiz', $eventname); 1320 // The method calendar_event::create will reuse a db record if the id field is set. 1321 calendar_event::create($event, false); 1322 } 1323 if ($timeclose && $addclose) { 1324 if ($oldevent = array_shift($oldevents)) { 1325 $event->id = $oldevent->id; 1326 } else { 1327 unset($event->id); 1328 } 1329 $event->type = CALENDAR_EVENT_TYPE_ACTION; 1330 $event->name = get_string('quizeventcloses', 'quiz', $eventname); 1331 $event->timestart = $timeclose; 1332 $event->timesort = $timeclose; 1333 $event->eventtype = QUIZ_EVENT_TYPE_CLOSE; 1334 if ($groupid && $grouppriorities !== null) { 1335 $closepriorities = $grouppriorities['close']; 1336 if (isset($closepriorities[$timeclose])) { 1337 $event->priority = $closepriorities[$timeclose]; 1338 } 1339 } 1340 calendar_event::create($event, false); 1341 } 1342 } 1343 } 1344 1345 // Delete any leftover events. 1346 foreach ($oldevents as $badevent) { 1347 $badevent = calendar_event::load($badevent); 1348 $badevent->delete(); 1349 } 1350 } 1351 1352 /** 1353 * Calculates the priorities of timeopen and timeclose values for group overrides for a quiz. 1354 * 1355 * @param int $quizid The quiz ID. 1356 * @return array|null Array of group override priorities for open and close times. Null if there are no group overrides. 1357 */ 1358 function quiz_get_group_override_priorities($quizid) { 1359 global $DB; 1360 1361 // Fetch group overrides. 1362 $where = 'quiz = :quiz AND groupid IS NOT NULL'; 1363 $params = ['quiz' => $quizid]; 1364 $overrides = $DB->get_records_select('quiz_overrides', $where, $params, '', 'id, timeopen, timeclose'); 1365 if (!$overrides) { 1366 return null; 1367 } 1368 1369 $grouptimeopen = []; 1370 $grouptimeclose = []; 1371 foreach ($overrides as $override) { 1372 if ($override->timeopen !== null && !in_array($override->timeopen, $grouptimeopen)) { 1373 $grouptimeopen[] = $override->timeopen; 1374 } 1375 if ($override->timeclose !== null && !in_array($override->timeclose, $grouptimeclose)) { 1376 $grouptimeclose[] = $override->timeclose; 1377 } 1378 } 1379 1380 // Sort open times in ascending manner. The earlier open time gets higher priority. 1381 sort($grouptimeopen); 1382 // Set priorities. 1383 $opengrouppriorities = []; 1384 $openpriority = 1; 1385 foreach ($grouptimeopen as $timeopen) { 1386 $opengrouppriorities[$timeopen] = $openpriority++; 1387 } 1388 1389 // Sort close times in descending manner. The later close time gets higher priority. 1390 rsort($grouptimeclose); 1391 // Set priorities. 1392 $closegrouppriorities = []; 1393 $closepriority = 1; 1394 foreach ($grouptimeclose as $timeclose) { 1395 $closegrouppriorities[$timeclose] = $closepriority++; 1396 } 1397 1398 return [ 1399 'open' => $opengrouppriorities, 1400 'close' => $closegrouppriorities 1401 ]; 1402 } 1403 1404 /** 1405 * List the actions that correspond to a view of this module. 1406 * This is used by the participation report. 1407 * 1408 * Note: This is not used by new logging system. Event with 1409 * crud = 'r' and edulevel = LEVEL_PARTICIPATING will 1410 * be considered as view action. 1411 * 1412 * @return array 1413 */ 1414 function quiz_get_view_actions() { 1415 return array('view', 'view all', 'report', 'review'); 1416 } 1417 1418 /** 1419 * List the actions that correspond to a post of this module. 1420 * This is used by the participation report. 1421 * 1422 * Note: This is not used by new logging system. Event with 1423 * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING 1424 * will be considered as post action. 1425 * 1426 * @return array 1427 */ 1428 function quiz_get_post_actions() { 1429 return array('attempt', 'close attempt', 'preview', 'editquestions', 1430 'delete attempt', 'manualgrade'); 1431 } 1432 1433 /** 1434 * Standard callback used by questions_in_use. 1435 * 1436 * @param array $questionids of question ids. 1437 * @return bool whether any of these questions are used by any instance of this module. 1438 */ 1439 function quiz_questions_in_use($questionids) { 1440 return question_engine::questions_in_use($questionids, 1441 new qubaid_join('{quiz_attempts} quiza', 'quiza.uniqueid', 1442 'quiza.preview = 0')); 1443 } 1444 1445 /** 1446 * Implementation of the function for printing the form elements that control 1447 * whether the course reset functionality affects the quiz. 1448 * 1449 * @param $mform the course reset form that is being built. 1450 */ 1451 function quiz_reset_course_form_definition($mform) { 1452 $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz')); 1453 $mform->addElement('advcheckbox', 'reset_quiz_attempts', 1454 get_string('removeallquizattempts', 'quiz')); 1455 $mform->addElement('advcheckbox', 'reset_quiz_user_overrides', 1456 get_string('removealluseroverrides', 'quiz')); 1457 $mform->addElement('advcheckbox', 'reset_quiz_group_overrides', 1458 get_string('removeallgroupoverrides', 'quiz')); 1459 } 1460 1461 /** 1462 * Course reset form defaults. 1463 * @return array the defaults. 1464 */ 1465 function quiz_reset_course_form_defaults($course) { 1466 return array('reset_quiz_attempts' => 1, 1467 'reset_quiz_group_overrides' => 1, 1468 'reset_quiz_user_overrides' => 1); 1469 } 1470 1471 /** 1472 * Removes all grades from gradebook 1473 * 1474 * @param int $courseid 1475 * @param string optional type 1476 */ 1477 function quiz_reset_gradebook($courseid, $type='') { 1478 global $CFG, $DB; 1479 1480 $quizzes = $DB->get_records_sql(" 1481 SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid 1482 FROM {modules} m 1483 JOIN {course_modules} cm ON m.id = cm.module 1484 JOIN {quiz} q ON cm.instance = q.id 1485 WHERE m.name = 'quiz' AND cm.course = ?", array($courseid)); 1486 1487 foreach ($quizzes as $quiz) { 1488 quiz_grade_item_update($quiz, 'reset'); 1489 } 1490 } 1491 1492 /** 1493 * Actual implementation of the reset course functionality, delete all the 1494 * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is 1495 * set and true. 1496 * 1497 * Also, move the quiz open and close dates, if the course start date is changing. 1498 * 1499 * @param object $data the data submitted from the reset course. 1500 * @return array status array 1501 */ 1502 function quiz_reset_userdata($data) { 1503 global $CFG, $DB; 1504 require_once($CFG->libdir . '/questionlib.php'); 1505 1506 $componentstr = get_string('modulenameplural', 'quiz'); 1507 $status = array(); 1508 1509 // Delete attempts. 1510 if (!empty($data->reset_quiz_attempts)) { 1511 question_engine::delete_questions_usage_by_activities(new qubaid_join( 1512 '{quiz_attempts} quiza JOIN {quiz} quiz ON quiza.quiz = quiz.id', 1513 'quiza.uniqueid', 'quiz.course = :quizcourseid', 1514 array('quizcourseid' => $data->courseid))); 1515 1516 $DB->delete_records_select('quiz_attempts', 1517 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid)); 1518 $status[] = array( 1519 'component' => $componentstr, 1520 'item' => get_string('attemptsdeleted', 'quiz'), 1521 'error' => false); 1522 1523 // Remove all grades from gradebook. 1524 $DB->delete_records_select('quiz_grades', 1525 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid)); 1526 if (empty($data->reset_gradebook_grades)) { 1527 quiz_reset_gradebook($data->courseid); 1528 } 1529 $status[] = array( 1530 'component' => $componentstr, 1531 'item' => get_string('gradesdeleted', 'quiz'), 1532 'error' => false); 1533 } 1534 1535 $purgeoverrides = false; 1536 1537 // Remove user overrides. 1538 if (!empty($data->reset_quiz_user_overrides)) { 1539 $DB->delete_records_select('quiz_overrides', 1540 'quiz IN (SELECT id FROM {quiz} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid)); 1541 $status[] = array( 1542 'component' => $componentstr, 1543 'item' => get_string('useroverridesdeleted', 'quiz'), 1544 'error' => false); 1545 $purgeoverrides = true; 1546 } 1547 // Remove group overrides. 1548 if (!empty($data->reset_quiz_group_overrides)) { 1549 $DB->delete_records_select('quiz_overrides', 1550 'quiz IN (SELECT id FROM {quiz} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid)); 1551 $status[] = array( 1552 'component' => $componentstr, 1553 'item' => get_string('groupoverridesdeleted', 'quiz'), 1554 'error' => false); 1555 $purgeoverrides = true; 1556 } 1557 1558 // Updating dates - shift may be negative too. 1559 if ($data->timeshift) { 1560 $DB->execute("UPDATE {quiz_overrides} 1561 SET timeopen = timeopen + ? 1562 WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?) 1563 AND timeopen <> 0", array($data->timeshift, $data->courseid)); 1564 $DB->execute("UPDATE {quiz_overrides} 1565 SET timeclose = timeclose + ? 1566 WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?) 1567 AND timeclose <> 0", array($data->timeshift, $data->courseid)); 1568 1569 $purgeoverrides = true; 1570 1571 // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. 1572 // See MDL-9367. 1573 shift_course_mod_dates('quiz', array('timeopen', 'timeclose'), 1574 $data->timeshift, $data->courseid); 1575 1576 $status[] = array( 1577 'component' => $componentstr, 1578 'item' => get_string('openclosedatesupdated', 'quiz'), 1579 'error' => false); 1580 } 1581 1582 if ($purgeoverrides) { 1583 cache::make('mod_quiz', 'overrides')->purge(); 1584 } 1585 1586 return $status; 1587 } 1588 1589 /** 1590 * @deprecated since Moodle 3.3, when the block_course_overview block was removed. 1591 */ 1592 function quiz_print_overview() { 1593 throw new coding_exception('quiz_print_overview() can not be used any more and is obsolete.'); 1594 } 1595 1596 /** 1597 * Return a textual summary of the number of attempts that have been made at a particular quiz, 1598 * returns '' if no attempts have been made yet, unless $returnzero is passed as true. 1599 * 1600 * @param object $quiz the quiz object. Only $quiz->id is used at the moment. 1601 * @param object $cm the cm object. Only $cm->course, $cm->groupmode and 1602 * $cm->groupingid fields are used at the moment. 1603 * @param bool $returnzero if false (default), when no attempts have been 1604 * made '' is returned instead of 'Attempts: 0'. 1605 * @param int $currentgroup if there is a concept of current group where this method is being called 1606 * (e.g. a report) pass it in here. Default 0 which means no current group. 1607 * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or 1608 * "Attemtps 123 (45 from this group)". 1609 */ 1610 function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) { 1611 global $DB, $USER; 1612 $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0)); 1613 if ($numattempts || $returnzero) { 1614 if (groups_get_activity_groupmode($cm)) { 1615 $a = new stdClass(); 1616 $a->total = $numattempts; 1617 if ($currentgroup) { 1618 $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' . 1619 '{quiz_attempts} qa JOIN ' . 1620 '{groups_members} gm ON qa.userid = gm.userid ' . 1621 'WHERE quiz = ? AND preview = 0 AND groupid = ?', 1622 array($quiz->id, $currentgroup)); 1623 return get_string('attemptsnumthisgroup', 'quiz', $a); 1624 } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) { 1625 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups)); 1626 $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' . 1627 '{quiz_attempts} qa JOIN ' . 1628 '{groups_members} gm ON qa.userid = gm.userid ' . 1629 'WHERE quiz = ? AND preview = 0 AND ' . 1630 "groupid $usql", array_merge(array($quiz->id), $params)); 1631 return get_string('attemptsnumyourgroups', 'quiz', $a); 1632 } 1633 } 1634 return get_string('attemptsnum', 'quiz', $numattempts); 1635 } 1636 return ''; 1637 } 1638 1639 /** 1640 * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link 1641 * to the quiz reports. 1642 * 1643 * @param object $quiz the quiz object. Only $quiz->id is used at the moment. 1644 * @param object $cm the cm object. Only $cm->course, $cm->groupmode and 1645 * $cm->groupingid fields are used at the moment. 1646 * @param object $context the quiz context. 1647 * @param bool $returnzero if false (default), when no attempts have been made 1648 * '' is returned instead of 'Attempts: 0'. 1649 * @param int $currentgroup if there is a concept of current group where this method is being called 1650 * (e.g. a report) pass it in here. Default 0 which means no current group. 1651 * @return string HTML fragment for the link. 1652 */ 1653 function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false, 1654 $currentgroup = 0) { 1655 global $PAGE; 1656 1657 return $PAGE->get_renderer('mod_quiz')->quiz_attempt_summary_link_to_reports( 1658 $quiz, $cm, $context, $returnzero, $currentgroup); 1659 } 1660 1661 /** 1662 * @param string $feature FEATURE_xx constant for requested feature 1663 * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. 1664 */ 1665 function quiz_supports($feature) { 1666 switch($feature) { 1667 case FEATURE_GROUPS: return true; 1668 case FEATURE_GROUPINGS: return true; 1669 case FEATURE_MOD_INTRO: return true; 1670 case FEATURE_COMPLETION_TRACKS_VIEWS: return true; 1671 case FEATURE_COMPLETION_HAS_RULES: return true; 1672 case FEATURE_GRADE_HAS_GRADE: return true; 1673 case FEATURE_GRADE_OUTCOMES: return true; 1674 case FEATURE_BACKUP_MOODLE2: return true; 1675 case FEATURE_SHOW_DESCRIPTION: return true; 1676 case FEATURE_CONTROLS_GRADE_VISIBILITY: return true; 1677 case FEATURE_USES_QUESTIONS: return true; 1678 case FEATURE_PLAGIARISM: return true; 1679 case FEATURE_MOD_PURPOSE: return MOD_PURPOSE_ASSESSMENT; 1680 1681 default: return null; 1682 } 1683 } 1684 1685 /** 1686 * @return array all other caps used in module 1687 */ 1688 function quiz_get_extra_capabilities() { 1689 global $CFG; 1690 require_once($CFG->libdir . '/questionlib.php'); 1691 return question_get_all_capabilities(); 1692 } 1693 1694 /** 1695 * This function extends the settings navigation block for the site. 1696 * 1697 * It is safe to rely on PAGE here as we will only ever be within the module 1698 * context when this is called 1699 * 1700 * @param settings_navigation $settings 1701 * @param navigation_node $quiznode 1702 * @return void 1703 */ 1704 function quiz_extend_settings_navigation(settings_navigation $settings, navigation_node $quiznode) { 1705 global $CFG; 1706 1707 // Require {@link questionlib.php} 1708 // Included here as we only ever want to include this file if we really need to. 1709 require_once($CFG->libdir . '/questionlib.php'); 1710 1711 // We want to add these new nodes after the Edit settings node, and before the 1712 // Locally assigned roles node. Of course, both of those are controlled by capabilities. 1713 $keys = $quiznode->get_children_key_list(); 1714 $beforekey = null; 1715 $i = array_search('modedit', $keys); 1716 if ($i === false and array_key_exists(0, $keys)) { 1717 $beforekey = $keys[0]; 1718 } else if (array_key_exists($i + 1, $keys)) { 1719 $beforekey = $keys[$i + 1]; 1720 } 1721 1722 if (has_any_capability(['mod/quiz:manageoverrides', 'mod/quiz:viewoverrides'], $settings->get_page()->cm->context)) { 1723 $url = new moodle_url('/mod/quiz/overrides.php', ['cmid' => $settings->get_page()->cm->id, 'mode' => 'user']); 1724 $node = navigation_node::create(get_string('overrides', 'quiz'), 1725 $url, navigation_node::TYPE_SETTING, null, 'mod_quiz_useroverrides'); 1726 $settingsoverride = $quiznode->add_node($node, $beforekey); 1727 } 1728 1729 if (has_capability('mod/quiz:manage', $settings->get_page()->cm->context)) { 1730 $node = navigation_node::create(get_string('questions', 'quiz'), 1731 new moodle_url('/mod/quiz/edit.php', array('cmid' => $settings->get_page()->cm->id)), 1732 navigation_node::TYPE_SETTING, null, 'mod_quiz_edit', new pix_icon('t/edit', '')); 1733 $quiznode->add_node($node, $beforekey); 1734 } 1735 1736 if (has_capability('mod/quiz:preview', $settings->get_page()->cm->context)) { 1737 $url = new moodle_url('/mod/quiz/startattempt.php', 1738 array('cmid' => $settings->get_page()->cm->id, 'sesskey' => sesskey())); 1739 $node = navigation_node::create(get_string('preview', 'quiz'), $url, 1740 navigation_node::TYPE_SETTING, null, 'mod_quiz_preview', 1741 new pix_icon('i/preview', '')); 1742 $previewnode = $quiznode->add_node($node, $beforekey); 1743 $previewnode->set_show_in_secondary_navigation(false); 1744 } 1745 1746 question_extend_settings_navigation($quiznode, $settings->get_page()->cm->context)->trim_if_empty(); 1747 1748 if (has_any_capability(array('mod/quiz:viewreports', 'mod/quiz:grade'), $settings->get_page()->cm->context)) { 1749 require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); 1750 $reportlist = quiz_report_list($settings->get_page()->cm->context); 1751 1752 $url = new moodle_url('/mod/quiz/report.php', 1753 array('id' => $settings->get_page()->cm->id, 'mode' => reset($reportlist))); 1754 $reportnode = $quiznode->add_node(navigation_node::create(get_string('results', 'quiz'), $url, 1755 navigation_node::TYPE_SETTING, 1756 null, 'quiz_report', new pix_icon('i/report', ''))); 1757 1758 foreach ($reportlist as $report) { 1759 $url = new moodle_url('/mod/quiz/report.php', ['id' => $settings->get_page()->cm->id, 'mode' => $report]); 1760 $reportnode->add_node(navigation_node::create(get_string($report, 'quiz_'.$report), $url, 1761 navigation_node::TYPE_SETTING, 1762 null, 'quiz_report_' . $report, new pix_icon('i/item', ''))); 1763 } 1764 } 1765 } 1766 1767 /** 1768 * Serves the quiz files. 1769 * 1770 * @package mod_quiz 1771 * @category files 1772 * @param stdClass $course course object 1773 * @param stdClass $cm course module object 1774 * @param stdClass $context context object 1775 * @param string $filearea file area 1776 * @param array $args extra arguments 1777 * @param bool $forcedownload whether or not force download 1778 * @param array $options additional options affecting the file serving 1779 * @return bool false if file not found, does not return if found - justsend the file 1780 */ 1781 function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { 1782 global $CFG, $DB; 1783 1784 if ($context->contextlevel != CONTEXT_MODULE) { 1785 return false; 1786 } 1787 1788 require_login($course, false, $cm); 1789 1790 if (!$quiz = $DB->get_record('quiz', array('id'=>$cm->instance))) { 1791 return false; 1792 } 1793 1794 // The 'intro' area is served by pluginfile.php. 1795 $fileareas = array('feedback'); 1796 if (!in_array($filearea, $fileareas)) { 1797 return false; 1798 } 1799 1800 $feedbackid = (int)array_shift($args); 1801 if (!$feedback = $DB->get_record('quiz_feedback', array('id'=>$feedbackid))) { 1802 return false; 1803 } 1804 1805 $fs = get_file_storage(); 1806 $relativepath = implode('/', $args); 1807 $fullpath = "/$context->id/mod_quiz/$filearea/$feedbackid/$relativepath"; 1808 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { 1809 return false; 1810 } 1811 send_stored_file($file, 0, 0, true, $options); 1812 } 1813 1814 /** 1815 * Called via pluginfile.php -> question_pluginfile to serve files belonging to 1816 * a question in a question_attempt when that attempt is a quiz attempt. 1817 * 1818 * @package mod_quiz 1819 * @category files 1820 * @param stdClass $course course settings object 1821 * @param stdClass $context context object 1822 * @param string $component the name of the component we are serving files for. 1823 * @param string $filearea the name of the file area. 1824 * @param int $qubaid the attempt usage id. 1825 * @param int $slot the id of a question in this quiz attempt. 1826 * @param array $args the remaining bits of the file path. 1827 * @param bool $forcedownload whether the user must be forced to download the file. 1828 * @param array $options additional options affecting the file serving 1829 * @return bool false if file not found, does not return if found - justsend the file 1830 */ 1831 function quiz_question_pluginfile($course, $context, $component, 1832 $filearea, $qubaid, $slot, $args, $forcedownload, array $options=array()) { 1833 global $CFG; 1834 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 1835 1836 $attemptobj = quiz_attempt::create_from_usage_id($qubaid); 1837 require_login($attemptobj->get_course(), false, $attemptobj->get_cm()); 1838 1839 if ($attemptobj->is_own_attempt() && !$attemptobj->is_finished()) { 1840 // In the middle of an attempt. 1841 if (!$attemptobj->is_preview_user()) { 1842 $attemptobj->require_capability('mod/quiz:attempt'); 1843 } 1844 $isreviewing = false; 1845 1846 } else { 1847 // Reviewing an attempt. 1848 $attemptobj->check_review_capability(); 1849 $isreviewing = true; 1850 } 1851 1852 if (!$attemptobj->check_file_access($slot, $isreviewing, $context->id, 1853 $component, $filearea, $args, $forcedownload)) { 1854 send_file_not_found(); 1855 } 1856 1857 $fs = get_file_storage(); 1858 $relativepath = implode('/', $args); 1859 $fullpath = "/$context->id/$component/$filearea/$relativepath"; 1860 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { 1861 send_file_not_found(); 1862 } 1863 1864 send_stored_file($file, 0, 0, $forcedownload, $options); 1865 } 1866 1867 /** 1868 * Return a list of page types 1869 * @param string $pagetype current page type 1870 * @param stdClass $parentcontext Block's parent context 1871 * @param stdClass $currentcontext Current context of block 1872 */ 1873 function quiz_page_type_list($pagetype, $parentcontext, $currentcontext) { 1874 $module_pagetype = array( 1875 'mod-quiz-*' => get_string('page-mod-quiz-x', 'quiz'), 1876 'mod-quiz-view' => get_string('page-mod-quiz-view', 'quiz'), 1877 'mod-quiz-attempt' => get_string('page-mod-quiz-attempt', 'quiz'), 1878 'mod-quiz-summary' => get_string('page-mod-quiz-summary', 'quiz'), 1879 'mod-quiz-review' => get_string('page-mod-quiz-review', 'quiz'), 1880 'mod-quiz-edit' => get_string('page-mod-quiz-edit', 'quiz'), 1881 'mod-quiz-report' => get_string('page-mod-quiz-report', 'quiz'), 1882 ); 1883 return $module_pagetype; 1884 } 1885 1886 /** 1887 * @return the options for quiz navigation. 1888 */ 1889 function quiz_get_navigation_options() { 1890 return array( 1891 QUIZ_NAVMETHOD_FREE => get_string('navmethod_free', 'quiz'), 1892 QUIZ_NAVMETHOD_SEQ => get_string('navmethod_seq', 'quiz') 1893 ); 1894 } 1895 1896 /** 1897 * Check if the module has any update that affects the current user since a given time. 1898 * 1899 * @param cm_info $cm course module data 1900 * @param int $from the time to check updates from 1901 * @param array $filter if we need to check only specific updates 1902 * @return stdClass an object with the different type of areas indicating if they were updated or not 1903 * @since Moodle 3.2 1904 */ 1905 function quiz_check_updates_since(cm_info $cm, $from, $filter = array()) { 1906 global $DB, $USER, $CFG; 1907 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 1908 1909 $updates = course_check_module_updates_since($cm, $from, array(), $filter); 1910 1911 // Check if questions were updated. 1912 $updates->questions = (object) array('updated' => false); 1913 $quizobj = quiz::create($cm->instance, $USER->id); 1914 $quizobj->preload_questions(); 1915 $quizobj->load_questions(); 1916 $questionids = array_keys($quizobj->get_questions()); 1917 if (!empty($questionids)) { 1918 list($questionsql, $params) = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED); 1919 $select = 'id ' . $questionsql . ' AND (timemodified > :time1 OR timecreated > :time2)'; 1920 $params['time1'] = $from; 1921 $params['time2'] = $from; 1922 $questions = $DB->get_records_select('question', $select, $params, '', 'id'); 1923 if (!empty($questions)) { 1924 $updates->questions->updated = true; 1925 $updates->questions->itemids = array_keys($questions); 1926 } 1927 } 1928 1929 // Check for new attempts or grades. 1930 $updates->attempts = (object) array('updated' => false); 1931 $updates->grades = (object) array('updated' => false); 1932 $select = 'quiz = ? AND userid = ? AND timemodified > ?'; 1933 $params = array($cm->instance, $USER->id, $from); 1934 1935 $attempts = $DB->get_records_select('quiz_attempts', $select, $params, '', 'id'); 1936 if (!empty($attempts)) { 1937 $updates->attempts->updated = true; 1938 $updates->attempts->itemids = array_keys($attempts); 1939 } 1940 $grades = $DB->get_records_select('quiz_grades', $select, $params, '', 'id'); 1941 if (!empty($grades)) { 1942 $updates->grades->updated = true; 1943 $updates->grades->itemids = array_keys($grades); 1944 } 1945 1946 // Now, teachers should see other students updates. 1947 if (has_capability('mod/quiz:viewreports', $cm->context)) { 1948 $select = 'quiz = ? AND timemodified > ?'; 1949 $params = array($cm->instance, $from); 1950 1951 if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) { 1952 $groupusers = array_keys(groups_get_activity_shared_group_members($cm)); 1953 if (empty($groupusers)) { 1954 return $updates; 1955 } 1956 list($insql, $inparams) = $DB->get_in_or_equal($groupusers); 1957 $select .= ' AND userid ' . $insql; 1958 $params = array_merge($params, $inparams); 1959 } 1960 1961 $updates->userattempts = (object) array('updated' => false); 1962 $attempts = $DB->get_records_select('quiz_attempts', $select, $params, '', 'id'); 1963 if (!empty($attempts)) { 1964 $updates->userattempts->updated = true; 1965 $updates->userattempts->itemids = array_keys($attempts); 1966 } 1967 1968 $updates->usergrades = (object) array('updated' => false); 1969 $grades = $DB->get_records_select('quiz_grades', $select, $params, '', 'id'); 1970 if (!empty($grades)) { 1971 $updates->usergrades->updated = true; 1972 $updates->usergrades->itemids = array_keys($grades); 1973 } 1974 } 1975 return $updates; 1976 } 1977 1978 /** 1979 * Get icon mapping for font-awesome. 1980 */ 1981 function mod_quiz_get_fontawesome_icon_map() { 1982 return [ 1983 'mod_quiz:navflagged' => 'fa-flag', 1984 ]; 1985 } 1986 1987 /** 1988 * This function receives a calendar event and returns the action associated with it, or null if there is none. 1989 * 1990 * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event 1991 * is not displayed on the block. 1992 * 1993 * @param calendar_event $event 1994 * @param \core_calendar\action_factory $factory 1995 * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). 1996 * @return \core_calendar\local\event\entities\action_interface|null 1997 */ 1998 function mod_quiz_core_calendar_provide_event_action(calendar_event $event, 1999 \core_calendar\action_factory $factory, 2000 int $userid = 0) { 2001 global $CFG, $USER; 2002 2003 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 2004 2005 if (empty($userid)) { 2006 $userid = $USER->id; 2007 } 2008 2009 $cm = get_fast_modinfo($event->courseid, $userid)->instances['quiz'][$event->instance]; 2010 $quizobj = quiz::create($cm->instance, $userid); 2011 $quiz = $quizobj->get_quiz(); 2012 2013 // Check they have capabilities allowing them to view the quiz. 2014 if (!has_any_capability(['mod/quiz:reviewmyattempts', 'mod/quiz:attempt'], $quizobj->get_context(), $userid)) { 2015 return null; 2016 } 2017 2018 $completion = new \completion_info($cm->get_course()); 2019 2020 $completiondata = $completion->get_data($cm, false, $userid); 2021 2022 if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { 2023 return null; 2024 } 2025 2026 quiz_update_effective_access($quiz, $userid); 2027 2028 // Check if quiz is closed, if so don't display it. 2029 if (!empty($quiz->timeclose) && $quiz->timeclose <= time()) { 2030 return null; 2031 } 2032 2033 if (!$quizobj->is_participant($userid)) { 2034 // If the user is not a participant then they have 2035 // no action to take. This will filter out the events for teachers. 2036 return null; 2037 } 2038 2039 $attempts = quiz_get_user_attempts($quizobj->get_quizid(), $userid); 2040 if (!empty($attempts)) { 2041 // The student's last attempt is finished. 2042 return null; 2043 } 2044 2045 $name = get_string('attemptquiznow', 'quiz'); 2046 $url = new \moodle_url('/mod/quiz/view.php', [ 2047 'id' => $cm->id 2048 ]); 2049 $itemcount = 1; 2050 $actionable = true; 2051 2052 // Check if the quiz is not currently actionable. 2053 if (!empty($quiz->timeopen) && $quiz->timeopen > time()) { 2054 $actionable = false; 2055 } 2056 2057 return $factory->create_instance( 2058 $name, 2059 $url, 2060 $itemcount, 2061 $actionable 2062 ); 2063 } 2064 2065 /** 2066 * Add a get_coursemodule_info function in case any quiz type wants to add 'extra' information 2067 * for the course (see resource). 2068 * 2069 * Given a course_module object, this function returns any "extra" information that may be needed 2070 * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. 2071 * 2072 * @param stdClass $coursemodule The coursemodule object (record). 2073 * @return cached_cm_info An object on information that the courses 2074 * will know about (most noticeably, an icon). 2075 */ 2076 function quiz_get_coursemodule_info($coursemodule) { 2077 global $DB; 2078 2079 $dbparams = ['id' => $coursemodule->instance]; 2080 $fields = 'id, name, intro, introformat, completionattemptsexhausted, completionminattempts, 2081 timeopen, timeclose'; 2082 if (!$quiz = $DB->get_record('quiz', $dbparams, $fields)) { 2083 return false; 2084 } 2085 2086 $result = new cached_cm_info(); 2087 $result->name = $quiz->name; 2088 2089 if ($coursemodule->showdescription) { 2090 // Convert intro to html. Do not filter cached version, filters run at display time. 2091 $result->content = format_module_intro('quiz', $quiz, $coursemodule->id, false); 2092 } 2093 2094 // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. 2095 if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { 2096 if ($quiz->completionattemptsexhausted) { 2097 $result->customdata['customcompletionrules']['completionpassorattemptsexhausted'] = [ 2098 'completionpassgrade' => $coursemodule->completionpassgrade, 2099 'completionattemptsexhausted' => $quiz->completionattemptsexhausted, 2100 ]; 2101 } else { 2102 $result->customdata['customcompletionrules']['completionpassorattemptsexhausted'] = []; 2103 } 2104 2105 $result->customdata['customcompletionrules']['completionminattempts'] = $quiz->completionminattempts; 2106 } 2107 2108 // Populate some other values that can be used in calendar or on dashboard. 2109 if ($quiz->timeopen) { 2110 $result->customdata['timeopen'] = $quiz->timeopen; 2111 } 2112 if ($quiz->timeclose) { 2113 $result->customdata['timeclose'] = $quiz->timeclose; 2114 } 2115 2116 return $result; 2117 } 2118 2119 /** 2120 * Sets dynamic information about a course module 2121 * 2122 * This function is called from cm_info when displaying the module 2123 * 2124 * @param cm_info $cm 2125 */ 2126 function mod_quiz_cm_info_dynamic(cm_info $cm) { 2127 global $USER; 2128 2129 $cache = cache::make('mod_quiz', 'overrides'); 2130 $override = $cache->get("{$cm->instance}_u_{$USER->id}"); 2131 2132 if (!$override) { 2133 $override = (object) [ 2134 'timeopen' => null, 2135 'timeclose' => null, 2136 ]; 2137 } 2138 2139 // No need to look for group overrides if there are user overrides for both timeopen and timeclose. 2140 if (is_null($override->timeopen) || is_null($override->timeclose)) { 2141 $opens = []; 2142 $closes = []; 2143 $groupings = groups_get_user_groups($cm->course, $USER->id); 2144 foreach ($groupings[0] as $groupid) { 2145 $groupoverride = $cache->get("{$cm->instance}_g_{$groupid}"); 2146 if (isset($groupoverride->timeopen)) { 2147 $opens[] = $groupoverride->timeopen; 2148 } 2149 if (isset($groupoverride->timeclose)) { 2150 $closes[] = $groupoverride->timeclose; 2151 } 2152 } 2153 // If there is a user override for a setting, ignore the group override. 2154 if (is_null($override->timeopen) && count($opens)) { 2155 $override->timeopen = min($opens); 2156 } 2157 if (is_null($override->timeclose) && count($closes)) { 2158 if (in_array(0, $closes)) { 2159 $override->timeclose = 0; 2160 } else { 2161 $override->timeclose = max($closes); 2162 } 2163 } 2164 } 2165 2166 // Populate some other values that can be used in calendar or on dashboard. 2167 if (!is_null($override->timeopen)) { 2168 $cm->override_customdata('timeopen', $override->timeopen); 2169 } 2170 if (!is_null($override->timeclose)) { 2171 $cm->override_customdata('timeclose', $override->timeclose); 2172 } 2173 } 2174 2175 /** 2176 * Callback which returns human-readable strings describing the active completion custom rules for the module instance. 2177 * 2178 * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules'] 2179 * @return array $descriptions the array of descriptions for the custom rules. 2180 */ 2181 function mod_quiz_get_completion_active_rule_descriptions($cm) { 2182 // Values will be present in cm_info, and we assume these are up to date. 2183 if (empty($cm->customdata['customcompletionrules']) 2184 || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) { 2185 return []; 2186 } 2187 2188 $descriptions = []; 2189 $rules = $cm->customdata['customcompletionrules']; 2190 2191 if (!empty($rules['completionpassorattemptsexhausted'])) { 2192 if (!empty($rules['completionpassorattemptsexhausted']['completionattemptsexhausted'])) { 2193 $descriptions[] = get_string('completionpassorattemptsexhausteddesc', 'quiz'); 2194 } 2195 } else { 2196 // Fallback. 2197 if (!empty($rules['completionattemptsexhausted'])) { 2198 $descriptions[] = get_string('completionpassorattemptsexhausteddesc', 'quiz'); 2199 } 2200 } 2201 2202 if (!empty($rules['completionminattempts'])) { 2203 $descriptions[] = get_string('completionminattemptsdesc', 'quiz', $rules['completionminattempts']); 2204 } 2205 2206 return $descriptions; 2207 } 2208 2209 /** 2210 * Returns the min and max values for the timestart property of a quiz 2211 * activity event. 2212 * 2213 * The min and max values will be the timeopen and timeclose properties 2214 * of the quiz, respectively, if they are set. 2215 * 2216 * If either value isn't set then null will be returned instead to 2217 * indicate that there is no cutoff for that value. 2218 * 2219 * If the vent has no valid timestart range then [false, false] will 2220 * be returned. This is the case for overriden events. 2221 * 2222 * A minimum and maximum cutoff return value will look like: 2223 * [ 2224 * [1505704373, 'The date must be after this date'], 2225 * [1506741172, 'The date must be before this date'] 2226 * ] 2227 * 2228 * @throws \moodle_exception 2229 * @param \calendar_event $event The calendar event to get the time range for 2230 * @param stdClass $quiz The module instance to get the range from 2231 * @return array 2232 */ 2233 function mod_quiz_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $quiz) { 2234 global $CFG, $DB; 2235 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 2236 2237 // Overrides do not have a valid timestart range. 2238 if (quiz_is_overriden_calendar_event($event)) { 2239 return [false, false]; 2240 } 2241 2242 $mindate = null; 2243 $maxdate = null; 2244 2245 if ($event->eventtype == QUIZ_EVENT_TYPE_OPEN) { 2246 if (!empty($quiz->timeclose)) { 2247 $maxdate = [ 2248 $quiz->timeclose, 2249 get_string('openafterclose', 'quiz') 2250 ]; 2251 } 2252 } else if ($event->eventtype == QUIZ_EVENT_TYPE_CLOSE) { 2253 if (!empty($quiz->timeopen)) { 2254 $mindate = [ 2255 $quiz->timeopen, 2256 get_string('closebeforeopen', 'quiz') 2257 ]; 2258 } 2259 } 2260 2261 return [$mindate, $maxdate]; 2262 } 2263 2264 /** 2265 * This function will update the quiz module according to the 2266 * event that has been modified. 2267 * 2268 * It will set the timeopen or timeclose value of the quiz instance 2269 * according to the type of event provided. 2270 * 2271 * @throws \moodle_exception 2272 * @param \calendar_event $event A quiz activity calendar event 2273 * @param \stdClass $quiz A quiz activity instance 2274 */ 2275 function mod_quiz_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $quiz) { 2276 global $CFG, $DB; 2277 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 2278 2279 if (!in_array($event->eventtype, [QUIZ_EVENT_TYPE_OPEN, QUIZ_EVENT_TYPE_CLOSE])) { 2280 // This isn't an event that we care about so we can ignore it. 2281 return; 2282 } 2283 2284 $courseid = $event->courseid; 2285 $modulename = $event->modulename; 2286 $instanceid = $event->instance; 2287 $modified = false; 2288 $closedatechanged = false; 2289 2290 // Something weird going on. The event is for a different module so 2291 // we should ignore it. 2292 if ($modulename != 'quiz') { 2293 return; 2294 } 2295 2296 if ($quiz->id != $instanceid) { 2297 // The provided quiz instance doesn't match the event so 2298 // there is nothing to do here. 2299 return; 2300 } 2301 2302 // We don't update the activity if it's an override event that has 2303 // been modified. 2304 if (quiz_is_overriden_calendar_event($event)) { 2305 return; 2306 } 2307 2308 $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; 2309 $context = context_module::instance($coursemodule->id); 2310 2311 // The user does not have the capability to modify this activity. 2312 if (!has_capability('moodle/course:manageactivities', $context)) { 2313 return; 2314 } 2315 2316 if ($event->eventtype == QUIZ_EVENT_TYPE_OPEN) { 2317 // If the event is for the quiz activity opening then we should 2318 // set the start time of the quiz activity to be the new start 2319 // time of the event. 2320 if ($quiz->timeopen != $event->timestart) { 2321 $quiz->timeopen = $event->timestart; 2322 $modified = true; 2323 } 2324 } else if ($event->eventtype == QUIZ_EVENT_TYPE_CLOSE) { 2325 // If the event is for the quiz activity closing then we should 2326 // set the end time of the quiz activity to be the new start 2327 // time of the event. 2328 if ($quiz->timeclose != $event->timestart) { 2329 $quiz->timeclose = $event->timestart; 2330 $modified = true; 2331 $closedatechanged = true; 2332 } 2333 } 2334 2335 if ($modified) { 2336 $quiz->timemodified = time(); 2337 $DB->update_record('quiz', $quiz); 2338 2339 if ($closedatechanged) { 2340 quiz_update_open_attempts(array('quizid' => $quiz->id)); 2341 } 2342 2343 // Delete any previous preview attempts. 2344 quiz_delete_previews($quiz); 2345 quiz_update_events($quiz); 2346 $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); 2347 $event->trigger(); 2348 } 2349 } 2350 2351 /** 2352 * Generates the question bank in a fragment output. This allows 2353 * the question bank to be displayed in a modal. 2354 * 2355 * The only expected argument provided in the $args array is 2356 * 'querystring'. The value should be the list of parameters 2357 * URL encoded and used to build the question bank page. 2358 * 2359 * The individual list of parameters expected can be found in 2360 * question_build_edit_resources. 2361 * 2362 * @param array $args The fragment arguments. 2363 * @return string The rendered mform fragment. 2364 */ 2365 function mod_quiz_output_fragment_quiz_question_bank($args) { 2366 global $CFG, $DB, $PAGE; 2367 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 2368 require_once($CFG->dirroot . '/question/editlib.php'); 2369 2370 $querystring = preg_replace('/^\?/', '', $args['querystring']); 2371 $params = []; 2372 parse_str($querystring, $params); 2373 2374 // Build the required resources. The $params are all cleaned as 2375 // part of this process. 2376 list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) = 2377 question_build_edit_resources('editq', '/mod/quiz/edit.php', $params); 2378 2379 // Get the course object and related bits. 2380 $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); 2381 require_capability('mod/quiz:manage', $contexts->lowest()); 2382 2383 // Create quiz question bank view. 2384 $questionbank = new mod_quiz\question\bank\custom_view($contexts, $thispageurl, $course, $cm, $quiz); 2385 $questionbank->set_quiz_has_attempts(quiz_has_attempts($quiz->id)); 2386 2387 // Output. 2388 $renderer = $PAGE->get_renderer('mod_quiz', 'edit'); 2389 return $renderer->question_bank_contents($questionbank, $pagevars); 2390 } 2391 2392 /** 2393 * Generates the add random question in a fragment output. This allows the 2394 * form to be rendered in javascript, for example inside a modal. 2395 * 2396 * The required arguments as keys in the $args array are: 2397 * cat {string} The category and category context ids comma separated. 2398 * addonpage {int} The page id to add this question to. 2399 * returnurl {string} URL to return to after form submission. 2400 * cmid {int} The course module id the questions are being added to. 2401 * 2402 * @param array $args The fragment arguments. 2403 * @return string The rendered mform fragment. 2404 */ 2405 function mod_quiz_output_fragment_add_random_question_form($args) { 2406 global $CFG; 2407 require_once($CFG->dirroot . '/mod/quiz/addrandomform.php'); 2408 2409 $contexts = new \core_question\local\bank\question_edit_contexts($args['context']); 2410 $formoptions = [ 2411 'contexts' => $contexts, 2412 'cat' => $args['cat'] 2413 ]; 2414 $formdata = [ 2415 'category' => $args['cat'], 2416 'addonpage' => $args['addonpage'], 2417 'returnurl' => $args['returnurl'], 2418 'cmid' => $args['cmid'] 2419 ]; 2420 2421 $form = new quiz_add_random_form( 2422 new \moodle_url('/mod/quiz/addrandom.php'), 2423 $formoptions, 2424 'post', 2425 '', 2426 null, 2427 true, 2428 $formdata 2429 ); 2430 $form->set_data($formdata); 2431 2432 return $form->render(); 2433 } 2434 2435 /** 2436 * Callback to fetch the activity event type lang string. 2437 * 2438 * @param string $eventtype The event type. 2439 * @return lang_string The event type lang string. 2440 */ 2441 function mod_quiz_core_calendar_get_event_action_string(string $eventtype): string { 2442 $modulename = get_string('modulename', 'quiz'); 2443 2444 switch ($eventtype) { 2445 case QUIZ_EVENT_TYPE_OPEN: 2446 $identifier = 'quizeventopens'; 2447 break; 2448 case QUIZ_EVENT_TYPE_CLOSE: 2449 $identifier = 'quizeventcloses'; 2450 break; 2451 default: 2452 return get_string('requiresaction', 'calendar', $modulename); 2453 } 2454 2455 return get_string($identifier, 'quiz', $modulename); 2456 } 2457 2458 /** 2459 * Delete question reference data. 2460 * 2461 * @param int $quizid The id of quiz. 2462 */ 2463 function quiz_delete_references($quizid): void { 2464 global $DB; 2465 $slots = $DB->get_records('quiz_slots', ['quizid' => $quizid]); 2466 foreach ($slots as $slot) { 2467 $params = [ 2468 'itemid' => $slot->id, 2469 'component' => 'mod_quiz', 2470 'questionarea' => 'slot' 2471 ]; 2472 // Delete any set references. 2473 $DB->delete_records('question_set_references', $params); 2474 // Delete any references. 2475 $DB->delete_records('question_references', $params); 2476 } 2477 } 2478 2479 /** 2480 * Implement the calculate_question_stats callback. 2481 * 2482 * This enables quiz statistics to be shown in statistics columns in the database. 2483 * 2484 * @param context $context return the statistics related to this context (which will be a quiz context). 2485 * @return all_calculated_for_qubaid_condition|null The statistics for this quiz, if available, else null. 2486 */ 2487 function mod_quiz_calculate_question_stats(context $context): ?all_calculated_for_qubaid_condition { 2488 global $CFG; 2489 require_once($CFG->dirroot . '/mod/quiz/report/statistics/report.php'); 2490 $cm = get_coursemodule_from_id('quiz', $context->instanceid); 2491 $report = new quiz_statistics_report(); 2492 return $report->calculate_questions_stats_for_question_bank($cm->instance, false); 2493 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body