[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
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 used by the quiz module. 19 * 20 * This contains functions that are called from within the quiz module only 21 * Functions that are also called by core Moodle are in {@link lib.php} 22 * This script also loads the code in {@link questionlib.php} which holds 23 * the module-indpendent code for handling questions and which in turn 24 * initialises all the questiontype classes. 25 * 26 * @package mod_quiz 27 * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} 28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 29 */ 30 31 32 defined('MOODLE_INTERNAL') || die(); 33 34 require_once($CFG->dirroot . '/mod/quiz/lib.php'); 35 require_once($CFG->dirroot . '/mod/quiz/accessmanager.php'); 36 require_once($CFG->dirroot . '/mod/quiz/accessmanager_form.php'); 37 require_once($CFG->dirroot . '/mod/quiz/renderer.php'); 38 require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); 39 require_once($CFG->libdir . '/eventslib.php'); 40 require_once($CFG->libdir . '/filelib.php'); 41 require_once($CFG->libdir . '/questionlib.php'); 42 43 44 /** 45 * @var int We show the countdown timer if there is less than this amount of time left before the 46 * the quiz close date. (1 hour) 47 */ 48 define('QUIZ_SHOW_TIME_BEFORE_DEADLINE', '3600'); 49 50 /** 51 * @var int If there are fewer than this many seconds left when the student submits 52 * a page of the quiz, then do not take them to the next page of the quiz. Instead 53 * close the quiz immediately. 54 */ 55 define('QUIZ_MIN_TIME_TO_CONTINUE', '2'); 56 57 /** 58 * @var int We show no image when user selects No image from dropdown menu in quiz settings. 59 */ 60 define('QUIZ_SHOWIMAGE_NONE', 0); 61 62 /** 63 * @var int We show small image when user selects small image from dropdown menu in quiz settings. 64 */ 65 define('QUIZ_SHOWIMAGE_SMALL', 1); 66 67 /** 68 * @var int We show Large image when user selects Large image from dropdown menu in quiz settings. 69 */ 70 define('QUIZ_SHOWIMAGE_LARGE', 2); 71 72 73 // Functions related to attempts /////////////////////////////////////////////// 74 75 /** 76 * Creates an object to represent a new attempt at a quiz 77 * 78 * Creates an attempt object to represent an attempt at the quiz by the current 79 * user starting at the current time. The ->id field is not set. The object is 80 * NOT written to the database. 81 * 82 * @param object $quizobj the quiz object to create an attempt for. 83 * @param int $attemptnumber the sequence number for the attempt. 84 * @param object $lastattempt the previous attempt by this user, if any. Only needed 85 * if $attemptnumber > 1 and $quiz->attemptonlast is true. 86 * @param int $timenow the time the attempt was started at. 87 * @param bool $ispreview whether this new attempt is a preview. 88 * @param int $userid the id of the user attempting this quiz. 89 * 90 * @return object the newly created attempt object. 91 */ 92 function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timenow, $ispreview = false, $userid = null) { 93 global $USER; 94 95 if ($userid === null) { 96 $userid = $USER->id; 97 } 98 99 $quiz = $quizobj->get_quiz(); 100 if ($quiz->sumgrades < 0.000005 && $quiz->grade > 0.000005) { 101 throw new moodle_exception('cannotstartgradesmismatch', 'quiz', 102 new moodle_url('/mod/quiz/view.php', array('q' => $quiz->id)), 103 array('grade' => quiz_format_grade($quiz, $quiz->grade))); 104 } 105 106 if ($attemptnumber == 1 || !$quiz->attemptonlast) { 107 // We are not building on last attempt so create a new attempt. 108 $attempt = new stdClass(); 109 $attempt->quiz = $quiz->id; 110 $attempt->userid = $userid; 111 $attempt->preview = 0; 112 $attempt->layout = ''; 113 } else { 114 // Build on last attempt. 115 if (empty($lastattempt)) { 116 print_error('cannotfindprevattempt', 'quiz'); 117 } 118 $attempt = $lastattempt; 119 } 120 121 $attempt->attempt = $attemptnumber; 122 $attempt->timestart = $timenow; 123 $attempt->timefinish = 0; 124 $attempt->timemodified = $timenow; 125 $attempt->state = quiz_attempt::IN_PROGRESS; 126 $attempt->currentpage = 0; 127 $attempt->sumgrades = null; 128 129 // If this is a preview, mark it as such. 130 if ($ispreview) { 131 $attempt->preview = 1; 132 } 133 134 $timeclose = $quizobj->get_access_manager($timenow)->get_end_time($attempt); 135 if ($timeclose === false || $ispreview) { 136 $attempt->timecheckstate = null; 137 } else { 138 $attempt->timecheckstate = $timeclose; 139 } 140 141 return $attempt; 142 } 143 /** 144 * Start a normal, new, quiz attempt. 145 * 146 * @param quiz $quizobj the quiz object to start an attempt for. 147 * @param question_usage_by_activity $quba 148 * @param object $attempt 149 * @param integer $attemptnumber starting from 1 150 * @param integer $timenow the attempt start time 151 * @param array $questionids slot number => question id. Used for random questions, to force the choice 152 * of a particular actual question. Intended for testing purposes only. 153 * @param array $forcedvariantsbyslot slot number => variant. Used for questions with variants, 154 * to force the choice of a particular variant. Intended for testing 155 * purposes only. 156 * @throws moodle_exception 157 * @return object modified attempt object 158 */ 159 function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, 160 $questionids = array(), $forcedvariantsbyslot = array()) { 161 // Fully load all the questions in this quiz. 162 $quizobj->preload_questions(); 163 $quizobj->load_questions(); 164 165 // Add them all to the $quba. 166 $questionsinuse = array_keys($quizobj->get_questions()); 167 foreach ($quizobj->get_questions() as $questiondata) { 168 if ($questiondata->qtype != 'random') { 169 if (!$quizobj->get_quiz()->shuffleanswers) { 170 $questiondata->options->shuffleanswers = false; 171 } 172 $question = question_bank::make_question($questiondata); 173 174 } else { 175 if (!isset($questionids[$quba->next_slot_number()])) { 176 $forcequestionid = null; 177 } else { 178 $forcequestionid = $questionids[$quba->next_slot_number()]; 179 } 180 181 $question = question_bank::get_qtype('random')->choose_other_question( 182 $questiondata, $questionsinuse, $quizobj->get_quiz()->shuffleanswers, $forcequestionid); 183 if (is_null($question)) { 184 throw new moodle_exception('notenoughrandomquestions', 'quiz', 185 $quizobj->view_url(), $questiondata); 186 } 187 } 188 189 $quba->add_question($question, $questiondata->maxmark); 190 $questionsinuse[] = $question->id; 191 } 192 193 // Start all the questions. 194 if ($attempt->preview) { 195 $variantoffset = rand(1, 100); 196 } else { 197 $variantoffset = $attemptnumber; 198 } 199 $variantstrategy = new question_variant_pseudorandom_no_repeats_strategy( 200 $variantoffset, $attempt->userid, $quizobj->get_quizid()); 201 202 if (!empty($forcedvariantsbyslot)) { 203 $forcedvariantsbyseed = question_variant_forced_choices_selection_strategy::prepare_forced_choices_array( 204 $forcedvariantsbyslot, $quba); 205 $variantstrategy = new question_variant_forced_choices_selection_strategy( 206 $forcedvariantsbyseed, $variantstrategy); 207 } 208 209 $quba->start_all_questions($variantstrategy, $timenow); 210 211 // Work out the attempt layout. 212 $layout = array(); 213 if ($quizobj->get_quiz()->shufflequestions) { 214 $slots = $quba->get_slots(); 215 shuffle($slots); 216 217 $questionsonthispage = 0; 218 foreach ($slots as $slot) { 219 if ($questionsonthispage && $questionsonthispage == $quizobj->get_quiz()->questionsperpage) { 220 $layout[] = 0; 221 $questionsonthispage = 0; 222 } 223 $layout[] = $slot; 224 $questionsonthispage += 1; 225 } 226 227 } else { 228 $currentpage = null; 229 foreach ($quizobj->get_questions() as $slot) { 230 if ($currentpage !== null && $slot->page != $currentpage) { 231 $layout[] = 0; 232 } 233 $layout[] = $slot->slot; 234 $currentpage = $slot->page; 235 } 236 } 237 238 $layout[] = 0; 239 $attempt->layout = implode(',', $layout); 240 241 return $attempt; 242 } 243 244 /** 245 * Start a subsequent new attempt, in each attempt builds on last mode. 246 * 247 * @param question_usage_by_activity $quba this question usage 248 * @param object $attempt this attempt 249 * @param object $lastattempt last attempt 250 * @return object modified attempt object 251 * 252 */ 253 function quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt) { 254 $oldquba = question_engine::load_questions_usage_by_activity($lastattempt->uniqueid); 255 256 $oldnumberstonew = array(); 257 foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) { 258 $newslot = $quba->add_question($oldqa->get_question(), $oldqa->get_max_mark()); 259 260 $quba->start_question_based_on($newslot, $oldqa); 261 262 $oldnumberstonew[$oldslot] = $newslot; 263 } 264 265 // Update attempt layout. 266 $newlayout = array(); 267 foreach (explode(',', $lastattempt->layout) as $oldslot) { 268 if ($oldslot != 0) { 269 $newlayout[] = $oldnumberstonew[$oldslot]; 270 } else { 271 $newlayout[] = 0; 272 } 273 } 274 $attempt->layout = implode(',', $newlayout); 275 return $attempt; 276 } 277 278 /** 279 * The save started question usage and quiz attempt in db and log the started attempt. 280 * 281 * @param quiz $quizobj 282 * @param question_usage_by_activity $quba 283 * @param object $attempt 284 * @return object attempt object with uniqueid and id set. 285 */ 286 function quiz_attempt_save_started($quizobj, $quba, $attempt) { 287 global $DB; 288 // Save the attempt in the database. 289 question_engine::save_questions_usage_by_activity($quba); 290 $attempt->uniqueid = $quba->get_id(); 291 $attempt->id = $DB->insert_record('quiz_attempts', $attempt); 292 293 // Params used by the events below. 294 $params = array( 295 'objectid' => $attempt->id, 296 'relateduserid' => $attempt->userid, 297 'courseid' => $quizobj->get_courseid(), 298 'context' => $quizobj->get_context() 299 ); 300 // Decide which event we are using. 301 if ($attempt->preview) { 302 $params['other'] = array( 303 'quizid' => $quizobj->get_quizid() 304 ); 305 $event = \mod_quiz\event\attempt_preview_started::create($params); 306 } else { 307 $event = \mod_quiz\event\attempt_started::create($params); 308 309 } 310 311 // Trigger the event. 312 $event->add_record_snapshot('quiz', $quizobj->get_quiz()); 313 $event->add_record_snapshot('quiz_attempts', $attempt); 314 $event->trigger(); 315 316 return $attempt; 317 } 318 319 /** 320 * Returns an unfinished attempt (if there is one) for the given 321 * user on the given quiz. This function does not return preview attempts. 322 * 323 * @param int $quizid the id of the quiz. 324 * @param int $userid the id of the user. 325 * 326 * @return mixed the unfinished attempt if there is one, false if not. 327 */ 328 function quiz_get_user_attempt_unfinished($quizid, $userid) { 329 $attempts = quiz_get_user_attempts($quizid, $userid, 'unfinished', true); 330 if ($attempts) { 331 return array_shift($attempts); 332 } else { 333 return false; 334 } 335 } 336 337 /** 338 * Delete a quiz attempt. 339 * @param mixed $attempt an integer attempt id or an attempt object 340 * (row of the quiz_attempts table). 341 * @param object $quiz the quiz object. 342 */ 343 function quiz_delete_attempt($attempt, $quiz) { 344 global $DB; 345 if (is_numeric($attempt)) { 346 if (!$attempt = $DB->get_record('quiz_attempts', array('id' => $attempt))) { 347 return; 348 } 349 } 350 351 if ($attempt->quiz != $quiz->id) { 352 debugging("Trying to delete attempt $attempt->id which belongs to quiz $attempt->quiz " . 353 "but was passed quiz $quiz->id."); 354 return; 355 } 356 357 if (!isset($quiz->cmid)) { 358 $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course); 359 $quiz->cmid = $cm->id; 360 } 361 362 question_engine::delete_questions_usage_by_activity($attempt->uniqueid); 363 $DB->delete_records('quiz_attempts', array('id' => $attempt->id)); 364 365 // Log the deletion of the attempt. 366 $params = array( 367 'objectid' => $attempt->id, 368 'relateduserid' => $attempt->userid, 369 'context' => context_module::instance($quiz->cmid), 370 'other' => array( 371 'quizid' => $quiz->id 372 ) 373 ); 374 $event = \mod_quiz\event\attempt_deleted::create($params); 375 $event->add_record_snapshot('quiz_attempts', $attempt); 376 $event->trigger(); 377 378 // Search quiz_attempts for other instances by this user. 379 // If none, then delete record for this quiz, this user from quiz_grades 380 // else recalculate best grade. 381 $userid = $attempt->userid; 382 if (!$DB->record_exists('quiz_attempts', array('userid' => $userid, 'quiz' => $quiz->id))) { 383 $DB->delete_records('quiz_grades', array('userid' => $userid, 'quiz' => $quiz->id)); 384 } else { 385 quiz_save_best_grade($quiz, $userid); 386 } 387 388 quiz_update_grades($quiz, $userid); 389 } 390 391 /** 392 * Delete all the preview attempts at a quiz, or possibly all the attempts belonging 393 * to one user. 394 * @param object $quiz the quiz object. 395 * @param int $userid (optional) if given, only delete the previews belonging to this user. 396 */ 397 function quiz_delete_previews($quiz, $userid = null) { 398 global $DB; 399 $conditions = array('quiz' => $quiz->id, 'preview' => 1); 400 if (!empty($userid)) { 401 $conditions['userid'] = $userid; 402 } 403 $previewattempts = $DB->get_records('quiz_attempts', $conditions); 404 foreach ($previewattempts as $attempt) { 405 quiz_delete_attempt($attempt, $quiz); 406 } 407 } 408 409 /** 410 * @param int $quizid The quiz id. 411 * @return bool whether this quiz has any (non-preview) attempts. 412 */ 413 function quiz_has_attempts($quizid) { 414 global $DB; 415 return $DB->record_exists('quiz_attempts', array('quiz' => $quizid, 'preview' => 0)); 416 } 417 418 // Functions to do with quiz layout and pages ////////////////////////////////// 419 420 /** 421 * Repaginate the questions in a quiz 422 * @param int $quizid the id of the quiz to repaginate. 423 * @param int $slotsperpage number of items to put on each page. 0 means unlimited. 424 */ 425 function quiz_repaginate_questions($quizid, $slotsperpage) { 426 global $DB; 427 $trans = $DB->start_delegated_transaction(); 428 429 $slots = $DB->get_records('quiz_slots', array('quizid' => $quizid), 430 'slot'); 431 432 $currentpage = 1; 433 $slotsonthispage = 0; 434 foreach ($slots as $slot) { 435 if ($slotsonthispage && $slotsonthispage == $slotsperpage) { 436 $currentpage += 1; 437 $slotsonthispage = 0; 438 } 439 if ($slot->page != $currentpage) { 440 $DB->set_field('quiz_slots', 'page', $currentpage, array('id' => $slot->id)); 441 } 442 $slotsonthispage += 1; 443 } 444 445 $trans->allow_commit(); 446 } 447 448 // Functions to do with quiz grades //////////////////////////////////////////// 449 450 /** 451 * Convert the raw grade stored in $attempt into a grade out of the maximum 452 * grade for this quiz. 453 * 454 * @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades 455 * @param object $quiz the quiz object. Only the fields grade, sumgrades and decimalpoints are used. 456 * @param bool|string $format whether to format the results for display 457 * or 'question' to format a question grade (different number of decimal places. 458 * @return float|string the rescaled grade, or null/the lang string 'notyetgraded' 459 * if the $grade is null. 460 */ 461 function quiz_rescale_grade($rawgrade, $quiz, $format = true) { 462 if (is_null($rawgrade)) { 463 $grade = null; 464 } else if ($quiz->sumgrades >= 0.000005) { 465 $grade = $rawgrade * $quiz->grade / $quiz->sumgrades; 466 } else { 467 $grade = 0; 468 } 469 if ($format === 'question') { 470 $grade = quiz_format_question_grade($quiz, $grade); 471 } else if ($format) { 472 $grade = quiz_format_grade($quiz, $grade); 473 } 474 return $grade; 475 } 476 477 /** 478 * Get the feedback text that should be show to a student who 479 * got this grade on this quiz. The feedback is processed ready for diplay. 480 * 481 * @param float $grade a grade on this quiz. 482 * @param object $quiz the quiz settings. 483 * @param object $context the quiz context. 484 * @return string the comment that corresponds to this grade (empty string if there is not one. 485 */ 486 function quiz_feedback_for_grade($grade, $quiz, $context) { 487 global $DB; 488 489 if (is_null($grade)) { 490 return ''; 491 } 492 493 // With CBM etc, it is possible to get -ve grades, which would then not match 494 // any feedback. Therefore, we replace -ve grades with 0. 495 $grade = max($grade, 0); 496 497 $feedback = $DB->get_record_select('quiz_feedback', 498 'quizid = ? AND mingrade <= ? AND ? < maxgrade', array($quiz->id, $grade, $grade)); 499 500 if (empty($feedback->feedbacktext)) { 501 return ''; 502 } 503 504 // Clean the text, ready for display. 505 $formatoptions = new stdClass(); 506 $formatoptions->noclean = true; 507 $feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php', 508 $context->id, 'mod_quiz', 'feedback', $feedback->id); 509 $feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions); 510 511 return $feedbacktext; 512 } 513 514 /** 515 * @param object $quiz the quiz database row. 516 * @return bool Whether this quiz has any non-blank feedback text. 517 */ 518 function quiz_has_feedback($quiz) { 519 global $DB; 520 static $cache = array(); 521 if (!array_key_exists($quiz->id, $cache)) { 522 $cache[$quiz->id] = quiz_has_grades($quiz) && 523 $DB->record_exists_select('quiz_feedback', "quizid = ? AND " . 524 $DB->sql_isnotempty('quiz_feedback', 'feedbacktext', false, true), 525 array($quiz->id)); 526 } 527 return $cache[$quiz->id]; 528 } 529 530 /** 531 * Update the sumgrades field of the quiz. This needs to be called whenever 532 * the grading structure of the quiz is changed. For example if a question is 533 * added or removed, or a question weight is changed. 534 * 535 * You should call {@link quiz_delete_previews()} before you call this function. 536 * 537 * @param object $quiz a quiz. 538 */ 539 function quiz_update_sumgrades($quiz) { 540 global $DB; 541 542 $sql = 'UPDATE {quiz} 543 SET sumgrades = COALESCE(( 544 SELECT SUM(maxmark) 545 FROM {quiz_slots} 546 WHERE quizid = {quiz}.id 547 ), 0) 548 WHERE id = ?'; 549 $DB->execute($sql, array($quiz->id)); 550 $quiz->sumgrades = $DB->get_field('quiz', 'sumgrades', array('id' => $quiz->id)); 551 552 if ($quiz->sumgrades < 0.000005 && quiz_has_attempts($quiz->id)) { 553 // If the quiz has been attempted, and the sumgrades has been 554 // set to 0, then we must also set the maximum possible grade to 0, or 555 // we will get a divide by zero error. 556 quiz_set_grade(0, $quiz); 557 } 558 } 559 560 /** 561 * Update the sumgrades field of the attempts at a quiz. 562 * 563 * @param object $quiz a quiz. 564 */ 565 function quiz_update_all_attempt_sumgrades($quiz) { 566 global $DB; 567 $dm = new question_engine_data_mapper(); 568 $timenow = time(); 569 570 $sql = "UPDATE {quiz_attempts} 571 SET 572 timemodified = :timenow, 573 sumgrades = ( 574 {$dm->sum_usage_marks_subquery('uniqueid')} 575 ) 576 WHERE quiz = :quizid AND state = :finishedstate"; 577 $DB->execute($sql, array('timenow' => $timenow, 'quizid' => $quiz->id, 578 'finishedstate' => quiz_attempt::FINISHED)); 579 } 580 581 /** 582 * The quiz grade is the maximum that student's results are marked out of. When it 583 * changes, the corresponding data in quiz_grades and quiz_feedback needs to be 584 * rescaled. After calling this function, you probably need to call 585 * quiz_update_all_attempt_sumgrades, quiz_update_all_final_grades and 586 * quiz_update_grades. 587 * 588 * @param float $newgrade the new maximum grade for the quiz. 589 * @param object $quiz the quiz we are updating. Passed by reference so its 590 * grade field can be updated too. 591 * @return bool indicating success or failure. 592 */ 593 function quiz_set_grade($newgrade, $quiz) { 594 global $DB; 595 // This is potentially expensive, so only do it if necessary. 596 if (abs($quiz->grade - $newgrade) < 1e-7) { 597 // Nothing to do. 598 return true; 599 } 600 601 $oldgrade = $quiz->grade; 602 $quiz->grade = $newgrade; 603 604 // Use a transaction, so that on those databases that support it, this is safer. 605 $transaction = $DB->start_delegated_transaction(); 606 607 // Update the quiz table. 608 $DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance)); 609 610 if ($oldgrade < 1) { 611 // If the old grade was zero, we cannot rescale, we have to recompute. 612 // We also recompute if the old grade was too small to avoid underflow problems. 613 quiz_update_all_final_grades($quiz); 614 615 } else { 616 // We can rescale the grades efficiently. 617 $timemodified = time(); 618 $DB->execute(" 619 UPDATE {quiz_grades} 620 SET grade = ? * grade, timemodified = ? 621 WHERE quiz = ? 622 ", array($newgrade/$oldgrade, $timemodified, $quiz->id)); 623 } 624 625 if ($oldgrade > 1e-7) { 626 // Update the quiz_feedback table. 627 $factor = $newgrade/$oldgrade; 628 $DB->execute(" 629 UPDATE {quiz_feedback} 630 SET mingrade = ? * mingrade, maxgrade = ? * maxgrade 631 WHERE quizid = ? 632 ", array($factor, $factor, $quiz->id)); 633 } 634 635 // Update grade item and send all grades to gradebook. 636 quiz_grade_item_update($quiz); 637 quiz_update_grades($quiz); 638 639 $transaction->allow_commit(); 640 return true; 641 } 642 643 /** 644 * Save the overall grade for a user at a quiz in the quiz_grades table 645 * 646 * @param object $quiz The quiz for which the best grade is to be calculated and then saved. 647 * @param int $userid The userid to calculate the grade for. Defaults to the current user. 648 * @param array $attempts The attempts of this user. Useful if you are 649 * looping through many users. Attempts can be fetched in one master query to 650 * avoid repeated querying. 651 * @return bool Indicates success or failure. 652 */ 653 function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) { 654 global $DB, $OUTPUT, $USER; 655 656 if (empty($userid)) { 657 $userid = $USER->id; 658 } 659 660 if (!$attempts) { 661 // Get all the attempts made by the user. 662 $attempts = quiz_get_user_attempts($quiz->id, $userid); 663 } 664 665 // Calculate the best grade. 666 $bestgrade = quiz_calculate_best_grade($quiz, $attempts); 667 $bestgrade = quiz_rescale_grade($bestgrade, $quiz, false); 668 669 // Save the best grade in the database. 670 if (is_null($bestgrade)) { 671 $DB->delete_records('quiz_grades', array('quiz' => $quiz->id, 'userid' => $userid)); 672 673 } else if ($grade = $DB->get_record('quiz_grades', 674 array('quiz' => $quiz->id, 'userid' => $userid))) { 675 $grade->grade = $bestgrade; 676 $grade->timemodified = time(); 677 $DB->update_record('quiz_grades', $grade); 678 679 } else { 680 $grade = new stdClass(); 681 $grade->quiz = $quiz->id; 682 $grade->userid = $userid; 683 $grade->grade = $bestgrade; 684 $grade->timemodified = time(); 685 $DB->insert_record('quiz_grades', $grade); 686 } 687 688 quiz_update_grades($quiz, $userid); 689 } 690 691 /** 692 * Calculate the overall grade for a quiz given a number of attempts by a particular user. 693 * 694 * @param object $quiz the quiz settings object. 695 * @param array $attempts an array of all the user's attempts at this quiz in order. 696 * @return float the overall grade 697 */ 698 function quiz_calculate_best_grade($quiz, $attempts) { 699 700 switch ($quiz->grademethod) { 701 702 case QUIZ_ATTEMPTFIRST: 703 $firstattempt = reset($attempts); 704 return $firstattempt->sumgrades; 705 706 case QUIZ_ATTEMPTLAST: 707 $lastattempt = end($attempts); 708 return $lastattempt->sumgrades; 709 710 case QUIZ_GRADEAVERAGE: 711 $sum = 0; 712 $count = 0; 713 foreach ($attempts as $attempt) { 714 if (!is_null($attempt->sumgrades)) { 715 $sum += $attempt->sumgrades; 716 $count++; 717 } 718 } 719 if ($count == 0) { 720 return null; 721 } 722 return $sum / $count; 723 724 case QUIZ_GRADEHIGHEST: 725 default: 726 $max = null; 727 foreach ($attempts as $attempt) { 728 if ($attempt->sumgrades > $max) { 729 $max = $attempt->sumgrades; 730 } 731 } 732 return $max; 733 } 734 } 735 736 /** 737 * Update the final grade at this quiz for all students. 738 * 739 * This function is equivalent to calling quiz_save_best_grade for all 740 * users, but much more efficient. 741 * 742 * @param object $quiz the quiz settings. 743 */ 744 function quiz_update_all_final_grades($quiz) { 745 global $DB; 746 747 if (!$quiz->sumgrades) { 748 return; 749 } 750 751 $param = array('iquizid' => $quiz->id, 'istatefinished' => quiz_attempt::FINISHED); 752 $firstlastattemptjoin = "JOIN ( 753 SELECT 754 iquiza.userid, 755 MIN(attempt) AS firstattempt, 756 MAX(attempt) AS lastattempt 757 758 FROM {quiz_attempts} iquiza 759 760 WHERE 761 iquiza.state = :istatefinished AND 762 iquiza.preview = 0 AND 763 iquiza.quiz = :iquizid 764 765 GROUP BY iquiza.userid 766 ) first_last_attempts ON first_last_attempts.userid = quiza.userid"; 767 768 switch ($quiz->grademethod) { 769 case QUIZ_ATTEMPTFIRST: 770 // Because of the where clause, there will only be one row, but we 771 // must still use an aggregate function. 772 $select = 'MAX(quiza.sumgrades)'; 773 $join = $firstlastattemptjoin; 774 $where = 'quiza.attempt = first_last_attempts.firstattempt AND'; 775 break; 776 777 case QUIZ_ATTEMPTLAST: 778 // Because of the where clause, there will only be one row, but we 779 // must still use an aggregate function. 780 $select = 'MAX(quiza.sumgrades)'; 781 $join = $firstlastattemptjoin; 782 $where = 'quiza.attempt = first_last_attempts.lastattempt AND'; 783 break; 784 785 case QUIZ_GRADEAVERAGE: 786 $select = 'AVG(quiza.sumgrades)'; 787 $join = ''; 788 $where = ''; 789 break; 790 791 default: 792 case QUIZ_GRADEHIGHEST: 793 $select = 'MAX(quiza.sumgrades)'; 794 $join = ''; 795 $where = ''; 796 break; 797 } 798 799 if ($quiz->sumgrades >= 0.000005) { 800 $finalgrade = $select . ' * ' . ($quiz->grade / $quiz->sumgrades); 801 } else { 802 $finalgrade = '0'; 803 } 804 $param['quizid'] = $quiz->id; 805 $param['quizid2'] = $quiz->id; 806 $param['quizid3'] = $quiz->id; 807 $param['quizid4'] = $quiz->id; 808 $param['statefinished'] = quiz_attempt::FINISHED; 809 $param['statefinished2'] = quiz_attempt::FINISHED; 810 $finalgradesubquery = " 811 SELECT quiza.userid, $finalgrade AS newgrade 812 FROM {quiz_attempts} quiza 813 $join 814 WHERE 815 $where 816 quiza.state = :statefinished AND 817 quiza.preview = 0 AND 818 quiza.quiz = :quizid3 819 GROUP BY quiza.userid"; 820 821 $changedgrades = $DB->get_records_sql(" 822 SELECT users.userid, qg.id, qg.grade, newgrades.newgrade 823 824 FROM ( 825 SELECT userid 826 FROM {quiz_grades} qg 827 WHERE quiz = :quizid 828 UNION 829 SELECT DISTINCT userid 830 FROM {quiz_attempts} quiza2 831 WHERE 832 quiza2.state = :statefinished2 AND 833 quiza2.preview = 0 AND 834 quiza2.quiz = :quizid2 835 ) users 836 837 LEFT JOIN {quiz_grades} qg ON qg.userid = users.userid AND qg.quiz = :quizid4 838 839 LEFT JOIN ( 840 $finalgradesubquery 841 ) newgrades ON newgrades.userid = users.userid 842 843 WHERE 844 ABS(newgrades.newgrade - qg.grade) > 0.000005 OR 845 ((newgrades.newgrade IS NULL OR qg.grade IS NULL) AND NOT 846 (newgrades.newgrade IS NULL AND qg.grade IS NULL))", 847 // The mess on the previous line is detecting where the value is 848 // NULL in one column, and NOT NULL in the other, but SQL does 849 // not have an XOR operator, and MS SQL server can't cope with 850 // (newgrades.newgrade IS NULL) <> (qg.grade IS NULL). 851 $param); 852 853 $timenow = time(); 854 $todelete = array(); 855 foreach ($changedgrades as $changedgrade) { 856 857 if (is_null($changedgrade->newgrade)) { 858 $todelete[] = $changedgrade->userid; 859 860 } else if (is_null($changedgrade->grade)) { 861 $toinsert = new stdClass(); 862 $toinsert->quiz = $quiz->id; 863 $toinsert->userid = $changedgrade->userid; 864 $toinsert->timemodified = $timenow; 865 $toinsert->grade = $changedgrade->newgrade; 866 $DB->insert_record('quiz_grades', $toinsert); 867 868 } else { 869 $toupdate = new stdClass(); 870 $toupdate->id = $changedgrade->id; 871 $toupdate->grade = $changedgrade->newgrade; 872 $toupdate->timemodified = $timenow; 873 $DB->update_record('quiz_grades', $toupdate); 874 } 875 } 876 877 if (!empty($todelete)) { 878 list($test, $params) = $DB->get_in_or_equal($todelete); 879 $DB->delete_records_select('quiz_grades', 'quiz = ? AND userid ' . $test, 880 array_merge(array($quiz->id), $params)); 881 } 882 } 883 884 /** 885 * Efficiently update check state time on all open attempts 886 * 887 * @param array $conditions optional restrictions on which attempts to update 888 * Allowed conditions: 889 * courseid => (array|int) attempts in given course(s) 890 * userid => (array|int) attempts for given user(s) 891 * quizid => (array|int) attempts in given quiz(s) 892 * groupid => (array|int) quizzes with some override for given group(s) 893 * 894 */ 895 function quiz_update_open_attempts(array $conditions) { 896 global $DB; 897 898 foreach ($conditions as &$value) { 899 if (!is_array($value)) { 900 $value = array($value); 901 } 902 } 903 904 $params = array(); 905 $wheres = array("quiza.state IN ('inprogress', 'overdue')"); 906 $iwheres = array("iquiza.state IN ('inprogress', 'overdue')"); 907 908 if (isset($conditions['courseid'])) { 909 list ($incond, $inparams) = $DB->get_in_or_equal($conditions['courseid'], SQL_PARAMS_NAMED, 'cid'); 910 $params = array_merge($params, $inparams); 911 $wheres[] = "quiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)"; 912 list ($incond, $inparams) = $DB->get_in_or_equal($conditions['courseid'], SQL_PARAMS_NAMED, 'icid'); 913 $params = array_merge($params, $inparams); 914 $iwheres[] = "iquiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)"; 915 } 916 917 if (isset($conditions['userid'])) { 918 list ($incond, $inparams) = $DB->get_in_or_equal($conditions['userid'], SQL_PARAMS_NAMED, 'uid'); 919 $params = array_merge($params, $inparams); 920 $wheres[] = "quiza.userid $incond"; 921 list ($incond, $inparams) = $DB->get_in_or_equal($conditions['userid'], SQL_PARAMS_NAMED, 'iuid'); 922 $params = array_merge($params, $inparams); 923 $iwheres[] = "iquiza.userid $incond"; 924 } 925 926 if (isset($conditions['quizid'])) { 927 list ($incond, $inparams) = $DB->get_in_or_equal($conditions['quizid'], SQL_PARAMS_NAMED, 'qid'); 928 $params = array_merge($params, $inparams); 929 $wheres[] = "quiza.quiz $incond"; 930 list ($incond, $inparams) = $DB->get_in_or_equal($conditions['quizid'], SQL_PARAMS_NAMED, 'iqid'); 931 $params = array_merge($params, $inparams); 932 $iwheres[] = "iquiza.quiz $incond"; 933 } 934 935 if (isset($conditions['groupid'])) { 936 list ($incond, $inparams) = $DB->get_in_or_equal($conditions['groupid'], SQL_PARAMS_NAMED, 'gid'); 937 $params = array_merge($params, $inparams); 938 $wheres[] = "quiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)"; 939 list ($incond, $inparams) = $DB->get_in_or_equal($conditions['groupid'], SQL_PARAMS_NAMED, 'igid'); 940 $params = array_merge($params, $inparams); 941 $iwheres[] = "iquiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)"; 942 } 943 944 // SQL to compute timeclose and timelimit for each attempt: 945 $quizausersql = quiz_get_attempt_usertime_sql( 946 implode("\n AND ", $iwheres)); 947 948 // SQL to compute the new timecheckstate 949 $timecheckstatesql = " 950 CASE WHEN quizauser.usertimelimit = 0 AND quizauser.usertimeclose = 0 THEN NULL 951 WHEN quizauser.usertimelimit = 0 THEN quizauser.usertimeclose 952 WHEN quizauser.usertimeclose = 0 THEN quiza.timestart + quizauser.usertimelimit 953 WHEN quiza.timestart + quizauser.usertimelimit < quizauser.usertimeclose THEN quiza.timestart + quizauser.usertimelimit 954 ELSE quizauser.usertimeclose END + 955 CASE WHEN quiza.state = 'overdue' THEN quiz.graceperiod ELSE 0 END"; 956 957 // SQL to select which attempts to process 958 $attemptselect = implode("\n AND ", $wheres); 959 960 /* 961 * Each database handles updates with inner joins differently: 962 * - mysql does not allow a FROM clause 963 * - postgres and mssql allow FROM but handle table aliases differently 964 * - oracle requires a subquery 965 * 966 * Different code for each database. 967 */ 968 969 $dbfamily = $DB->get_dbfamily(); 970 if ($dbfamily == 'mysql') { 971 $updatesql = "UPDATE {quiz_attempts} quiza 972 JOIN {quiz} quiz ON quiz.id = quiza.quiz 973 JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id 974 SET quiza.timecheckstate = $timecheckstatesql 975 WHERE $attemptselect"; 976 } else if ($dbfamily == 'postgres') { 977 $updatesql = "UPDATE {quiz_attempts} quiza 978 SET timecheckstate = $timecheckstatesql 979 FROM {quiz} quiz, ( $quizausersql ) quizauser 980 WHERE quiz.id = quiza.quiz 981 AND quizauser.id = quiza.id 982 AND $attemptselect"; 983 } else if ($dbfamily == 'mssql') { 984 $updatesql = "UPDATE quiza 985 SET timecheckstate = $timecheckstatesql 986 FROM {quiz_attempts} quiza 987 JOIN {quiz} quiz ON quiz.id = quiza.quiz 988 JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id 989 WHERE $attemptselect"; 990 } else { 991 // oracle, sqlite and others 992 $updatesql = "UPDATE {quiz_attempts} quiza 993 SET timecheckstate = ( 994 SELECT $timecheckstatesql 995 FROM {quiz} quiz, ( $quizausersql ) quizauser 996 WHERE quiz.id = quiza.quiz 997 AND quizauser.id = quiza.id 998 ) 999 WHERE $attemptselect"; 1000 } 1001 1002 $DB->execute($updatesql, $params); 1003 } 1004 1005 /** 1006 * Returns SQL to compute timeclose and timelimit for every attempt, taking into account user and group overrides. 1007 * 1008 * @param string $redundantwhereclauses extra where clauses to add to the subquery 1009 * for performance. These can use the table alias iquiza for the quiz attempts table. 1010 * @return string SQL select with columns attempt.id, usertimeclose, usertimelimit. 1011 */ 1012 function quiz_get_attempt_usertime_sql($redundantwhereclauses = '') { 1013 if ($redundantwhereclauses) { 1014 $redundantwhereclauses = 'WHERE ' . $redundantwhereclauses; 1015 } 1016 // The multiple qgo JOINS are necessary because we want timeclose/timelimit = 0 (unlimited) to supercede 1017 // any other group override 1018 $quizausersql = " 1019 SELECT iquiza.id, 1020 COALESCE(MAX(quo.timeclose), MAX(qgo1.timeclose), MAX(qgo2.timeclose), iquiz.timeclose) AS usertimeclose, 1021 COALESCE(MAX(quo.timelimit), MAX(qgo3.timelimit), MAX(qgo4.timelimit), iquiz.timelimit) AS usertimelimit 1022 1023 FROM {quiz_attempts} iquiza 1024 JOIN {quiz} iquiz ON iquiz.id = iquiza.quiz 1025 LEFT JOIN {quiz_overrides} quo ON quo.quiz = iquiza.quiz AND quo.userid = iquiza.userid 1026 LEFT JOIN {groups_members} gm ON gm.userid = iquiza.userid 1027 LEFT JOIN {quiz_overrides} qgo1 ON qgo1.quiz = iquiza.quiz AND qgo1.groupid = gm.groupid AND qgo1.timeclose = 0 1028 LEFT JOIN {quiz_overrides} qgo2 ON qgo2.quiz = iquiza.quiz AND qgo2.groupid = gm.groupid AND qgo2.timeclose > 0 1029 LEFT JOIN {quiz_overrides} qgo3 ON qgo3.quiz = iquiza.quiz AND qgo3.groupid = gm.groupid AND qgo3.timelimit = 0 1030 LEFT JOIN {quiz_overrides} qgo4 ON qgo4.quiz = iquiza.quiz AND qgo4.groupid = gm.groupid AND qgo4.timelimit > 0 1031 $redundantwhereclauses 1032 GROUP BY iquiza.id, iquiz.id, iquiz.timeclose, iquiz.timelimit"; 1033 return $quizausersql; 1034 } 1035 1036 /** 1037 * Return the attempt with the best grade for a quiz 1038 * 1039 * Which attempt is the best depends on $quiz->grademethod. If the grade 1040 * method is GRADEAVERAGE then this function simply returns the last attempt. 1041 * @return object The attempt with the best grade 1042 * @param object $quiz The quiz for which the best grade is to be calculated 1043 * @param array $attempts An array of all the attempts of the user at the quiz 1044 */ 1045 function quiz_calculate_best_attempt($quiz, $attempts) { 1046 1047 switch ($quiz->grademethod) { 1048 1049 case QUIZ_ATTEMPTFIRST: 1050 foreach ($attempts as $attempt) { 1051 return $attempt; 1052 } 1053 break; 1054 1055 case QUIZ_GRADEAVERAGE: // We need to do something with it. 1056 case QUIZ_ATTEMPTLAST: 1057 foreach ($attempts as $attempt) { 1058 $final = $attempt; 1059 } 1060 return $final; 1061 1062 default: 1063 case QUIZ_GRADEHIGHEST: 1064 $max = -1; 1065 foreach ($attempts as $attempt) { 1066 if ($attempt->sumgrades > $max) { 1067 $max = $attempt->sumgrades; 1068 $maxattempt = $attempt; 1069 } 1070 } 1071 return $maxattempt; 1072 } 1073 } 1074 1075 /** 1076 * @return array int => lang string the options for calculating the quiz grade 1077 * from the individual attempt grades. 1078 */ 1079 function quiz_get_grading_options() { 1080 return array( 1081 QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'), 1082 QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'), 1083 QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'), 1084 QUIZ_ATTEMPTLAST => get_string('attemptlast', 'quiz') 1085 ); 1086 } 1087 1088 /** 1089 * @param int $option one of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE, 1090 * QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST. 1091 * @return the lang string for that option. 1092 */ 1093 function quiz_get_grading_option_name($option) { 1094 $strings = quiz_get_grading_options(); 1095 return $strings[$option]; 1096 } 1097 1098 /** 1099 * @return array string => lang string the options for handling overdue quiz 1100 * attempts. 1101 */ 1102 function quiz_get_overdue_handling_options() { 1103 return array( 1104 'autosubmit' => get_string('overduehandlingautosubmit', 'quiz'), 1105 'graceperiod' => get_string('overduehandlinggraceperiod', 'quiz'), 1106 'autoabandon' => get_string('overduehandlingautoabandon', 'quiz'), 1107 ); 1108 } 1109 1110 /** 1111 * Get the choices for what size user picture to show. 1112 * @return array string => lang string the options for whether to display the user's picture. 1113 */ 1114 function quiz_get_user_image_options() { 1115 return array( 1116 QUIZ_SHOWIMAGE_NONE => get_string('shownoimage', 'quiz'), 1117 QUIZ_SHOWIMAGE_SMALL => get_string('showsmallimage', 'quiz'), 1118 QUIZ_SHOWIMAGE_LARGE => get_string('showlargeimage', 'quiz'), 1119 ); 1120 } 1121 1122 /** 1123 * Get the choices to offer for the 'Questions per page' option. 1124 * @return array int => string. 1125 */ 1126 function quiz_questions_per_page_options() { 1127 $pageoptions = array(); 1128 $pageoptions[0] = get_string('neverallononepage', 'quiz'); 1129 $pageoptions[1] = get_string('everyquestion', 'quiz'); 1130 for ($i = 2; $i <= QUIZ_MAX_QPP_OPTION; ++$i) { 1131 $pageoptions[$i] = get_string('everynquestions', 'quiz', $i); 1132 } 1133 return $pageoptions; 1134 } 1135 1136 /** 1137 * Get the human-readable name for a quiz attempt state. 1138 * @param string $state one of the state constants like {@link quiz_attempt::IN_PROGRESS}. 1139 * @return string The lang string to describe that state. 1140 */ 1141 function quiz_attempt_state_name($state) { 1142 switch ($state) { 1143 case quiz_attempt::IN_PROGRESS: 1144 return get_string('stateinprogress', 'quiz'); 1145 case quiz_attempt::OVERDUE: 1146 return get_string('stateoverdue', 'quiz'); 1147 case quiz_attempt::FINISHED: 1148 return get_string('statefinished', 'quiz'); 1149 case quiz_attempt::ABANDONED: 1150 return get_string('stateabandoned', 'quiz'); 1151 default: 1152 throw new coding_exception('Unknown quiz attempt state.'); 1153 } 1154 } 1155 1156 // Other quiz functions //////////////////////////////////////////////////////// 1157 1158 /** 1159 * @param object $quiz the quiz. 1160 * @param int $cmid the course_module object for this quiz. 1161 * @param object $question the question. 1162 * @param string $returnurl url to return to after action is done. 1163 * @return string html for a number of icons linked to action pages for a 1164 * question - preview and edit / view icons depending on user capabilities. 1165 */ 1166 function quiz_question_action_icons($quiz, $cmid, $question, $returnurl) { 1167 $html = quiz_question_preview_button($quiz, $question) . ' ' . 1168 quiz_question_edit_button($cmid, $question, $returnurl); 1169 return $html; 1170 } 1171 1172 /** 1173 * @param int $cmid the course_module.id for this quiz. 1174 * @param object $question the question. 1175 * @param string $returnurl url to return to after action is done. 1176 * @param string $contentbeforeicon some HTML content to be added inside the link, before the icon. 1177 * @return the HTML for an edit icon, view icon, or nothing for a question 1178 * (depending on permissions). 1179 */ 1180 function quiz_question_edit_button($cmid, $question, $returnurl, $contentaftericon = '') { 1181 global $CFG, $OUTPUT; 1182 1183 // Minor efficiency saving. Only get strings once, even if there are a lot of icons on one page. 1184 static $stredit = null; 1185 static $strview = null; 1186 if ($stredit === null) { 1187 $stredit = get_string('edit'); 1188 $strview = get_string('view'); 1189 } 1190 1191 // What sort of icon should we show? 1192 $action = ''; 1193 if (!empty($question->id) && 1194 (question_has_capability_on($question, 'edit', $question->category) || 1195 question_has_capability_on($question, 'move', $question->category))) { 1196 $action = $stredit; 1197 $icon = '/t/edit'; 1198 } else if (!empty($question->id) && 1199 question_has_capability_on($question, 'view', $question->category)) { 1200 $action = $strview; 1201 $icon = '/i/info'; 1202 } 1203 1204 // Build the icon. 1205 if ($action) { 1206 if ($returnurl instanceof moodle_url) { 1207 $returnurl = $returnurl->out_as_local_url(false); 1208 } 1209 $questionparams = array('returnurl' => $returnurl, 'cmid' => $cmid, 'id' => $question->id); 1210 $questionurl = new moodle_url("$CFG->wwwroot/question/question.php", $questionparams); 1211 return '<a title="' . $action . '" href="' . $questionurl->out() . '" class="questioneditbutton"><img src="' . 1212 $OUTPUT->pix_url($icon) . '" alt="' . $action . '" />' . $contentaftericon . 1213 '</a>'; 1214 } else if ($contentaftericon) { 1215 return '<span class="questioneditbutton">' . $contentaftericon . '</span>'; 1216 } else { 1217 return ''; 1218 } 1219 } 1220 1221 /** 1222 * @param object $quiz the quiz settings 1223 * @param object $question the question 1224 * @return moodle_url to preview this question with the options from this quiz. 1225 */ 1226 function quiz_question_preview_url($quiz, $question) { 1227 // Get the appropriate display options. 1228 $displayoptions = mod_quiz_display_options::make_from_quiz($quiz, 1229 mod_quiz_display_options::DURING); 1230 1231 $maxmark = null; 1232 if (isset($question->maxmark)) { 1233 $maxmark = $question->maxmark; 1234 } 1235 1236 // Work out the correcte preview URL. 1237 return question_preview_url($question->id, $quiz->preferredbehaviour, 1238 $maxmark, $displayoptions); 1239 } 1240 1241 /** 1242 * @param object $quiz the quiz settings 1243 * @param object $question the question 1244 * @param bool $label if true, show the preview question label after the icon 1245 * @return the HTML for a preview question icon. 1246 */ 1247 function quiz_question_preview_button($quiz, $question, $label = false) { 1248 global $PAGE; 1249 if (!question_has_capability_on($question, 'use', $question->category)) { 1250 return ''; 1251 } 1252 1253 return $PAGE->get_renderer('mod_quiz', 'edit')->question_preview_icon($quiz, $question, $label); 1254 } 1255 1256 /** 1257 * @param object $attempt the attempt. 1258 * @param object $context the quiz context. 1259 * @return int whether flags should be shown/editable to the current user for this attempt. 1260 */ 1261 function quiz_get_flag_option($attempt, $context) { 1262 global $USER; 1263 if (!has_capability('moodle/question:flag', $context)) { 1264 return question_display_options::HIDDEN; 1265 } else if ($attempt->userid == $USER->id) { 1266 return question_display_options::EDITABLE; 1267 } else { 1268 return question_display_options::VISIBLE; 1269 } 1270 } 1271 1272 /** 1273 * Work out what state this quiz attempt is in - in the sense used by 1274 * quiz_get_review_options, not in the sense of $attempt->state. 1275 * @param object $quiz the quiz settings 1276 * @param object $attempt the quiz_attempt database row. 1277 * @return int one of the mod_quiz_display_options::DURING, 1278 * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. 1279 */ 1280 function quiz_attempt_state($quiz, $attempt) { 1281 if ($attempt->state == quiz_attempt::IN_PROGRESS) { 1282 return mod_quiz_display_options::DURING; 1283 } else if (time() < $attempt->timefinish + 120) { 1284 return mod_quiz_display_options::IMMEDIATELY_AFTER; 1285 } else if (!$quiz->timeclose || time() < $quiz->timeclose) { 1286 return mod_quiz_display_options::LATER_WHILE_OPEN; 1287 } else { 1288 return mod_quiz_display_options::AFTER_CLOSE; 1289 } 1290 } 1291 1292 /** 1293 * The the appropraite mod_quiz_display_options object for this attempt at this 1294 * quiz right now. 1295 * 1296 * @param object $quiz the quiz instance. 1297 * @param object $attempt the attempt in question. 1298 * @param $context the quiz context. 1299 * 1300 * @return mod_quiz_display_options 1301 */ 1302 function quiz_get_review_options($quiz, $attempt, $context) { 1303 $options = mod_quiz_display_options::make_from_quiz($quiz, quiz_attempt_state($quiz, $attempt)); 1304 1305 $options->readonly = true; 1306 $options->flags = quiz_get_flag_option($attempt, $context); 1307 if (!empty($attempt->id)) { 1308 $options->questionreviewlink = new moodle_url('/mod/quiz/reviewquestion.php', 1309 array('attempt' => $attempt->id)); 1310 } 1311 1312 // Show a link to the comment box only for closed attempts. 1313 if (!empty($attempt->id) && $attempt->state == quiz_attempt::FINISHED && !$attempt->preview && 1314 !is_null($context) && has_capability('mod/quiz:grade', $context)) { 1315 $options->manualcomment = question_display_options::VISIBLE; 1316 $options->manualcommentlink = new moodle_url('/mod/quiz/comment.php', 1317 array('attempt' => $attempt->id)); 1318 } 1319 1320 if (!is_null($context) && !$attempt->preview && 1321 has_capability('mod/quiz:viewreports', $context) && 1322 has_capability('moodle/grade:viewhidden', $context)) { 1323 // People who can see reports and hidden grades should be shown everything, 1324 // except during preview when teachers want to see what students see. 1325 $options->attempt = question_display_options::VISIBLE; 1326 $options->correctness = question_display_options::VISIBLE; 1327 $options->marks = question_display_options::MARK_AND_MAX; 1328 $options->feedback = question_display_options::VISIBLE; 1329 $options->numpartscorrect = question_display_options::VISIBLE; 1330 $options->manualcomment = question_display_options::VISIBLE; 1331 $options->generalfeedback = question_display_options::VISIBLE; 1332 $options->rightanswer = question_display_options::VISIBLE; 1333 $options->overallfeedback = question_display_options::VISIBLE; 1334 $options->history = question_display_options::VISIBLE; 1335 1336 } 1337 1338 return $options; 1339 } 1340 1341 /** 1342 * Combines the review options from a number of different quiz attempts. 1343 * Returns an array of two ojects, so the suggested way of calling this 1344 * funciton is: 1345 * list($someoptions, $alloptions) = quiz_get_combined_reviewoptions(...) 1346 * 1347 * @param object $quiz the quiz instance. 1348 * @param array $attempts an array of attempt objects. 1349 * @param $context the roles and permissions context, 1350 * normally the context for the quiz module instance. 1351 * 1352 * @return array of two options objects, one showing which options are true for 1353 * at least one of the attempts, the other showing which options are true 1354 * for all attempts. 1355 */ 1356 function quiz_get_combined_reviewoptions($quiz, $attempts) { 1357 $fields = array('feedback', 'generalfeedback', 'rightanswer', 'overallfeedback'); 1358 $someoptions = new stdClass(); 1359 $alloptions = new stdClass(); 1360 foreach ($fields as $field) { 1361 $someoptions->$field = false; 1362 $alloptions->$field = true; 1363 } 1364 $someoptions->marks = question_display_options::HIDDEN; 1365 $alloptions->marks = question_display_options::MARK_AND_MAX; 1366 1367 foreach ($attempts as $attempt) { 1368 $attemptoptions = mod_quiz_display_options::make_from_quiz($quiz, 1369 quiz_attempt_state($quiz, $attempt)); 1370 foreach ($fields as $field) { 1371 $someoptions->$field = $someoptions->$field || $attemptoptions->$field; 1372 $alloptions->$field = $alloptions->$field && $attemptoptions->$field; 1373 } 1374 $someoptions->marks = max($someoptions->marks, $attemptoptions->marks); 1375 $alloptions->marks = min($alloptions->marks, $attemptoptions->marks); 1376 } 1377 return array($someoptions, $alloptions); 1378 } 1379 1380 // Functions for sending notification messages ///////////////////////////////// 1381 1382 /** 1383 * Sends a confirmation message to the student confirming that the attempt was processed. 1384 * 1385 * @param object $a lots of useful information that can be used in the message 1386 * subject and body. 1387 * 1388 * @return int|false as for {@link message_send()}. 1389 */ 1390 function quiz_send_confirmation($recipient, $a) { 1391 1392 // Add information about the recipient to $a. 1393 // Don't do idnumber. we want idnumber to be the submitter's idnumber. 1394 $a->username = fullname($recipient); 1395 $a->userusername = $recipient->username; 1396 1397 // Prepare the message. 1398 $eventdata = new stdClass(); 1399 $eventdata->component = 'mod_quiz'; 1400 $eventdata->name = 'confirmation'; 1401 $eventdata->notification = 1; 1402 1403 $eventdata->userfrom = core_user::get_noreply_user(); 1404 $eventdata->userto = $recipient; 1405 $eventdata->subject = get_string('emailconfirmsubject', 'quiz', $a); 1406 $eventdata->fullmessage = get_string('emailconfirmbody', 'quiz', $a); 1407 $eventdata->fullmessageformat = FORMAT_PLAIN; 1408 $eventdata->fullmessagehtml = ''; 1409 1410 $eventdata->smallmessage = get_string('emailconfirmsmall', 'quiz', $a); 1411 $eventdata->contexturl = $a->quizurl; 1412 $eventdata->contexturlname = $a->quizname; 1413 1414 // ... and send it. 1415 return message_send($eventdata); 1416 } 1417 1418 /** 1419 * Sends notification messages to the interested parties that assign the role capability 1420 * 1421 * @param object $recipient user object of the intended recipient 1422 * @param object $a associative array of replaceable fields for the templates 1423 * 1424 * @return int|false as for {@link message_send()}. 1425 */ 1426 function quiz_send_notification($recipient, $submitter, $a) { 1427 1428 // Recipient info for template. 1429 $a->useridnumber = $recipient->idnumber; 1430 $a->username = fullname($recipient); 1431 $a->userusername = $recipient->username; 1432 1433 // Prepare the message. 1434 $eventdata = new stdClass(); 1435 $eventdata->component = 'mod_quiz'; 1436 $eventdata->name = 'submission'; 1437 $eventdata->notification = 1; 1438 1439 $eventdata->userfrom = $submitter; 1440 $eventdata->userto = $recipient; 1441 $eventdata->subject = get_string('emailnotifysubject', 'quiz', $a); 1442 $eventdata->fullmessage = get_string('emailnotifybody', 'quiz', $a); 1443 $eventdata->fullmessageformat = FORMAT_PLAIN; 1444 $eventdata->fullmessagehtml = ''; 1445 1446 $eventdata->smallmessage = get_string('emailnotifysmall', 'quiz', $a); 1447 $eventdata->contexturl = $a->quizreviewurl; 1448 $eventdata->contexturlname = $a->quizname; 1449 1450 // ... and send it. 1451 return message_send($eventdata); 1452 } 1453 1454 /** 1455 * Send all the requried messages when a quiz attempt is submitted. 1456 * 1457 * @param object $course the course 1458 * @param object $quiz the quiz 1459 * @param object $attempt this attempt just finished 1460 * @param object $context the quiz context 1461 * @param object $cm the coursemodule for this quiz 1462 * 1463 * @return bool true if all necessary messages were sent successfully, else false. 1464 */ 1465 function quiz_send_notification_messages($course, $quiz, $attempt, $context, $cm) { 1466 global $CFG, $DB; 1467 1468 // Do nothing if required objects not present. 1469 if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) { 1470 throw new coding_exception('$course, $quiz, $attempt, $context and $cm must all be set.'); 1471 } 1472 1473 $submitter = $DB->get_record('user', array('id' => $attempt->userid), '*', MUST_EXIST); 1474 1475 // Check for confirmation required. 1476 $sendconfirm = false; 1477 $notifyexcludeusers = ''; 1478 if (has_capability('mod/quiz:emailconfirmsubmission', $context, $submitter, false)) { 1479 $notifyexcludeusers = $submitter->id; 1480 $sendconfirm = true; 1481 } 1482 1483 // Check for notifications required. 1484 $notifyfields = 'u.id, u.username, u.idnumber, u.email, u.emailstop, u.lang, u.timezone, u.mailformat, u.maildisplay, '; 1485 $notifyfields .= get_all_user_name_fields(true, 'u'); 1486 $groups = groups_get_all_groups($course->id, $submitter->id); 1487 if (is_array($groups) && count($groups) > 0) { 1488 $groups = array_keys($groups); 1489 } else if (groups_get_activity_groupmode($cm, $course) != NOGROUPS) { 1490 // If the user is not in a group, and the quiz is set to group mode, 1491 // then set $groups to a non-existant id so that only users with 1492 // 'moodle/site:accessallgroups' get notified. 1493 $groups = -1; 1494 } else { 1495 $groups = ''; 1496 } 1497 $userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission', 1498 $notifyfields, '', '', '', $groups, $notifyexcludeusers, false, false, true); 1499 1500 if (empty($userstonotify) && !$sendconfirm) { 1501 return true; // Nothing to do. 1502 } 1503 1504 $a = new stdClass(); 1505 // Course info. 1506 $a->coursename = $course->fullname; 1507 $a->courseshortname = $course->shortname; 1508 // Quiz info. 1509 $a->quizname = $quiz->name; 1510 $a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?id=' . $cm->id; 1511 $a->quizreportlink = '<a href="' . $a->quizreporturl . '">' . 1512 format_string($quiz->name) . ' report</a>'; 1513 $a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id; 1514 $a->quizlink = '<a href="' . $a->quizurl . '">' . format_string($quiz->name) . '</a>'; 1515 // Attempt info. 1516 $a->submissiontime = userdate($attempt->timefinish); 1517 $a->timetaken = format_time($attempt->timefinish - $attempt->timestart); 1518 $a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id; 1519 $a->quizreviewlink = '<a href="' . $a->quizreviewurl . '">' . 1520 format_string($quiz->name) . ' review</a>'; 1521 // Student who sat the quiz info. 1522 $a->studentidnumber = $submitter->idnumber; 1523 $a->studentname = fullname($submitter); 1524 $a->studentusername = $submitter->username; 1525 1526 $allok = true; 1527 1528 // Send notifications if required. 1529 if (!empty($userstonotify)) { 1530 foreach ($userstonotify as $recipient) { 1531 $allok = $allok && quiz_send_notification($recipient, $submitter, $a); 1532 } 1533 } 1534 1535 // Send confirmation if required. We send the student confirmation last, so 1536 // that if message sending is being intermittently buggy, which means we send 1537 // some but not all messages, and then try again later, then teachers may get 1538 // duplicate messages, but the student will always get exactly one. 1539 if ($sendconfirm) { 1540 $allok = $allok && quiz_send_confirmation($submitter, $a); 1541 } 1542 1543 return $allok; 1544 } 1545 1546 /** 1547 * Send the notification message when a quiz attempt becomes overdue. 1548 * 1549 * @param quiz_attempt $attemptobj all the data about the quiz attempt. 1550 */ 1551 function quiz_send_overdue_message($attemptobj) { 1552 global $CFG, $DB; 1553 1554 $submitter = $DB->get_record('user', array('id' => $attemptobj->get_userid()), '*', MUST_EXIST); 1555 1556 if (!$attemptobj->has_capability('mod/quiz:emailwarnoverdue', $submitter->id, false)) { 1557 return; // Message not required. 1558 } 1559 1560 if (!$attemptobj->has_response_to_at_least_one_graded_question()) { 1561 return; // Message not required. 1562 } 1563 1564 // Prepare lots of useful information that admins might want to include in 1565 // the email message. 1566 $quizname = format_string($attemptobj->get_quiz_name()); 1567 1568 $deadlines = array(); 1569 if ($attemptobj->get_quiz()->timelimit) { 1570 $deadlines[] = $attemptobj->get_attempt()->timestart + $attemptobj->get_quiz()->timelimit; 1571 } 1572 if ($attemptobj->get_quiz()->timeclose) { 1573 $deadlines[] = $attemptobj->get_quiz()->timeclose; 1574 } 1575 $duedate = min($deadlines); 1576 $graceend = $duedate + $attemptobj->get_quiz()->graceperiod; 1577 1578 $a = new stdClass(); 1579 // Course info. 1580 $a->coursename = format_string($attemptobj->get_course()->fullname); 1581 $a->courseshortname = format_string($attemptobj->get_course()->shortname); 1582 // Quiz info. 1583 $a->quizname = $quizname; 1584 $a->quizurl = $attemptobj->view_url(); 1585 $a->quizlink = '<a href="' . $a->quizurl . '">' . $quizname . '</a>'; 1586 // Attempt info. 1587 $a->attemptduedate = userdate($duedate); 1588 $a->attemptgraceend = userdate($graceend); 1589 $a->attemptsummaryurl = $attemptobj->summary_url()->out(false); 1590 $a->attemptsummarylink = '<a href="' . $a->attemptsummaryurl . '">' . $quizname . ' review</a>'; 1591 // Student's info. 1592 $a->studentidnumber = $submitter->idnumber; 1593 $a->studentname = fullname($submitter); 1594 $a->studentusername = $submitter->username; 1595 1596 // Prepare the message. 1597 $eventdata = new stdClass(); 1598 $eventdata->component = 'mod_quiz'; 1599 $eventdata->name = 'attempt_overdue'; 1600 $eventdata->notification = 1; 1601 1602 $eventdata->userfrom = core_user::get_noreply_user(); 1603 $eventdata->userto = $submitter; 1604 $eventdata->subject = get_string('emailoverduesubject', 'quiz', $a); 1605 $eventdata->fullmessage = get_string('emailoverduebody', 'quiz', $a); 1606 $eventdata->fullmessageformat = FORMAT_PLAIN; 1607 $eventdata->fullmessagehtml = ''; 1608 1609 $eventdata->smallmessage = get_string('emailoverduesmall', 'quiz', $a); 1610 $eventdata->contexturl = $a->quizurl; 1611 $eventdata->contexturlname = $a->quizname; 1612 1613 // Send the message. 1614 return message_send($eventdata); 1615 } 1616 1617 /** 1618 * Handle the quiz_attempt_submitted event. 1619 * 1620 * This sends the confirmation and notification messages, if required. 1621 * 1622 * @param object $event the event object. 1623 */ 1624 function quiz_attempt_submitted_handler($event) { 1625 global $DB; 1626 1627 $course = $DB->get_record('course', array('id' => $event->courseid)); 1628 $attempt = $event->get_record_snapshot('quiz_attempts', $event->objectid); 1629 $quiz = $event->get_record_snapshot('quiz', $attempt->quiz); 1630 $cm = get_coursemodule_from_id('quiz', $event->get_context()->instanceid, $event->courseid); 1631 1632 if (!($course && $quiz && $cm && $attempt)) { 1633 // Something has been deleted since the event was raised. Therefore, the 1634 // event is no longer relevant. 1635 return true; 1636 } 1637 1638 // Update completion state. 1639 $completion = new completion_info($course); 1640 if ($completion->is_enabled($cm) && ($quiz->completionattemptsexhausted || $quiz->completionpass)) { 1641 $completion->update_state($cm, COMPLETION_COMPLETE, $event->userid); 1642 } 1643 return quiz_send_notification_messages($course, $quiz, $attempt, 1644 context_module::instance($cm->id), $cm); 1645 } 1646 1647 /** 1648 * Handle groups_member_added event 1649 * 1650 * @param object $event the event object. 1651 * @deprecated since 2.6, see {@link \mod_quiz\group_observers::group_member_added()}. 1652 */ 1653 function quiz_groups_member_added_handler($event) { 1654 debugging('quiz_groups_member_added_handler() is deprecated, please use ' . 1655 '\mod_quiz\group_observers::group_member_added() instead.', DEBUG_DEVELOPER); 1656 quiz_update_open_attempts(array('userid'=>$event->userid, 'groupid'=>$event->groupid)); 1657 } 1658 1659 /** 1660 * Handle groups_member_removed event 1661 * 1662 * @param object $event the event object. 1663 * @deprecated since 2.6, see {@link \mod_quiz\group_observers::group_member_removed()}. 1664 */ 1665 function quiz_groups_member_removed_handler($event) { 1666 debugging('quiz_groups_member_removed_handler() is deprecated, please use ' . 1667 '\mod_quiz\group_observers::group_member_removed() instead.', DEBUG_DEVELOPER); 1668 quiz_update_open_attempts(array('userid'=>$event->userid, 'groupid'=>$event->groupid)); 1669 } 1670 1671 /** 1672 * Handle groups_group_deleted event 1673 * 1674 * @param object $event the event object. 1675 * @deprecated since 2.6, see {@link \mod_quiz\group_observers::group_deleted()}. 1676 */ 1677 function quiz_groups_group_deleted_handler($event) { 1678 global $DB; 1679 debugging('quiz_groups_group_deleted_handler() is deprecated, please use ' . 1680 '\mod_quiz\group_observers::group_deleted() instead.', DEBUG_DEVELOPER); 1681 quiz_process_group_deleted_in_course($event->courseid); 1682 } 1683 1684 /** 1685 * Logic to happen when a/some group(s) has/have been deleted in a course. 1686 * 1687 * @param int $courseid The course ID. 1688 * @return void 1689 */ 1690 function quiz_process_group_deleted_in_course($courseid) { 1691 global $DB; 1692 1693 // It would be nice if we got the groupid that was deleted. 1694 // Instead, we just update all quizzes with orphaned group overrides. 1695 $sql = "SELECT o.id, o.quiz 1696 FROM {quiz_overrides} o 1697 JOIN {quiz} quiz ON quiz.id = o.quiz 1698 LEFT JOIN {groups} grp ON grp.id = o.groupid 1699 WHERE quiz.course = :courseid 1700 AND o.groupid IS NOT NULL 1701 AND grp.id IS NULL"; 1702 $params = array('courseid' => $courseid); 1703 $records = $DB->get_records_sql_menu($sql, $params); 1704 if (!$records) { 1705 return; // Nothing to do. 1706 } 1707 $DB->delete_records_list('quiz_overrides', 'id', array_keys($records)); 1708 quiz_update_open_attempts(array('quizid' => array_unique(array_values($records)))); 1709 } 1710 1711 /** 1712 * Handle groups_members_removed event 1713 * 1714 * @param object $event the event object. 1715 * @deprecated since 2.6, see {@link \mod_quiz\group_observers::group_member_removed()}. 1716 */ 1717 function quiz_groups_members_removed_handler($event) { 1718 debugging('quiz_groups_members_removed_handler() is deprecated, please use ' . 1719 '\mod_quiz\group_observers::group_member_removed() instead.', DEBUG_DEVELOPER); 1720 if ($event->userid == 0) { 1721 quiz_update_open_attempts(array('courseid'=>$event->courseid)); 1722 } else { 1723 quiz_update_open_attempts(array('courseid'=>$event->courseid, 'userid'=>$event->userid)); 1724 } 1725 } 1726 1727 /** 1728 * Get the information about the standard quiz JavaScript module. 1729 * @return array a standard jsmodule structure. 1730 */ 1731 function quiz_get_js_module() { 1732 global $PAGE; 1733 1734 return array( 1735 'name' => 'mod_quiz', 1736 'fullpath' => '/mod/quiz/module.js', 1737 'requires' => array('base', 'dom', 'event-delegate', 'event-key', 1738 'core_question_engine', 'moodle-core-formchangechecker'), 1739 'strings' => array( 1740 array('cancel', 'moodle'), 1741 array('flagged', 'question'), 1742 array('functiondisabledbysecuremode', 'quiz'), 1743 array('startattempt', 'quiz'), 1744 array('timesup', 'quiz'), 1745 array('changesmadereallygoaway', 'moodle'), 1746 ), 1747 ); 1748 } 1749 1750 1751 /** 1752 * An extension of question_display_options that includes the extra options used 1753 * by the quiz. 1754 * 1755 * @copyright 2010 The Open University 1756 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1757 */ 1758 class mod_quiz_display_options extends question_display_options { 1759 /**#@+ 1760 * @var integer bits used to indicate various times in relation to a 1761 * quiz attempt. 1762 */ 1763 const DURING = 0x10000; 1764 const IMMEDIATELY_AFTER = 0x01000; 1765 const LATER_WHILE_OPEN = 0x00100; 1766 const AFTER_CLOSE = 0x00010; 1767 /**#@-*/ 1768 1769 /** 1770 * @var boolean if this is false, then the student is not allowed to review 1771 * anything about the attempt. 1772 */ 1773 public $attempt = true; 1774 1775 /** 1776 * @var boolean if this is false, then the student is not allowed to review 1777 * anything about the attempt. 1778 */ 1779 public $overallfeedback = self::VISIBLE; 1780 1781 /** 1782 * Set up the various options from the quiz settings, and a time constant. 1783 * @param object $quiz the quiz settings. 1784 * @param int $one of the {@link DURING}, {@link IMMEDIATELY_AFTER}, 1785 * {@link LATER_WHILE_OPEN} or {@link AFTER_CLOSE} constants. 1786 * @return mod_quiz_display_options set up appropriately. 1787 */ 1788 public static function make_from_quiz($quiz, $when) { 1789 $options = new self(); 1790 1791 $options->attempt = self::extract($quiz->reviewattempt, $when, true, false); 1792 $options->correctness = self::extract($quiz->reviewcorrectness, $when); 1793 $options->marks = self::extract($quiz->reviewmarks, $when, 1794 self::MARK_AND_MAX, self::MAX_ONLY); 1795 $options->feedback = self::extract($quiz->reviewspecificfeedback, $when); 1796 $options->generalfeedback = self::extract($quiz->reviewgeneralfeedback, $when); 1797 $options->rightanswer = self::extract($quiz->reviewrightanswer, $when); 1798 $options->overallfeedback = self::extract($quiz->reviewoverallfeedback, $when); 1799 1800 $options->numpartscorrect = $options->feedback; 1801 $options->manualcomment = $options->feedback; 1802 1803 if ($quiz->questiondecimalpoints != -1) { 1804 $options->markdp = $quiz->questiondecimalpoints; 1805 } else { 1806 $options->markdp = $quiz->decimalpoints; 1807 } 1808 1809 return $options; 1810 } 1811 1812 protected static function extract($bitmask, $bit, 1813 $whenset = self::VISIBLE, $whennotset = self::HIDDEN) { 1814 if ($bitmask & $bit) { 1815 return $whenset; 1816 } else { 1817 return $whennotset; 1818 } 1819 } 1820 } 1821 1822 1823 /** 1824 * A {@link qubaid_condition} for finding all the question usages belonging to 1825 * a particular quiz. 1826 * 1827 * @copyright 2010 The Open University 1828 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1829 */ 1830 class qubaids_for_quiz extends qubaid_join { 1831 public function __construct($quizid, $includepreviews = true, $onlyfinished = false) { 1832 $where = 'quiza.quiz = :quizaquiz'; 1833 $params = array('quizaquiz' => $quizid); 1834 1835 if (!$includepreviews) { 1836 $where .= ' AND preview = 0'; 1837 } 1838 1839 if ($onlyfinished) { 1840 $where .= ' AND state == :statefinished'; 1841 $params['statefinished'] = quiz_attempt::FINISHED; 1842 } 1843 1844 parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params); 1845 } 1846 } 1847 1848 /** 1849 * Creates a textual representation of a question for display. 1850 * 1851 * @param object $question A question object from the database questions table 1852 * @param bool $showicon If true, show the question's icon with the question. False by default. 1853 * @param bool $showquestiontext If true (default), show question text after question name. 1854 * If false, show only question name. 1855 * @return string 1856 */ 1857 function quiz_question_tostring($question, $showicon = false, $showquestiontext = true) { 1858 $result = ''; 1859 1860 $name = shorten_text(format_string($question->name), 200); 1861 if ($showicon) { 1862 $name .= print_question_icon($question) . ' ' . $name; 1863 } 1864 $result .= html_writer::span($name, 'questionname'); 1865 1866 if ($showquestiontext) { 1867 $questiontext = question_utils::to_plain_text($question->questiontext, 1868 $question->questiontextformat, array('noclean' => true, 'para' => false)); 1869 $questiontext = shorten_text($questiontext, 200); 1870 if ($questiontext) { 1871 $result .= ' ' . html_writer::span(s($questiontext), 'questiontext'); 1872 } 1873 } 1874 1875 return $result; 1876 } 1877 1878 /** 1879 * Verify that the question exists, and the user has permission to use it. 1880 * Does not return. Throws an exception if the question cannot be used. 1881 * @param int $questionid The id of the question. 1882 */ 1883 function quiz_require_question_use($questionid) { 1884 global $DB; 1885 $question = $DB->get_record('question', array('id' => $questionid), '*', MUST_EXIST); 1886 question_require_capability_on($question, 'use'); 1887 } 1888 1889 /** 1890 * Verify that the question exists, and the user has permission to use it. 1891 * @param object $quiz the quiz settings. 1892 * @param int $slot which question in the quiz to test. 1893 * @return bool whether the user can use this question. 1894 */ 1895 function quiz_has_question_use($quiz, $slot) { 1896 global $DB; 1897 $question = $DB->get_record_sql(" 1898 SELECT q.* 1899 FROM {quiz_slots} slot 1900 JOIN {question} q ON q.id = slot.questionid 1901 WHERE slot.quizid = ? AND slot.slot = ?", array($quiz->id, $slot)); 1902 if (!$question) { 1903 return false; 1904 } 1905 return question_has_capability_on($question, 'use'); 1906 } 1907 1908 /** 1909 * Add a question to a quiz 1910 * 1911 * Adds a question to a quiz by updating $quiz as well as the 1912 * quiz and quiz_slots tables. It also adds a page break if required. 1913 * @param int $questionid The id of the question to be added 1914 * @param object $quiz The extended quiz object as used by edit.php 1915 * This is updated by this function 1916 * @param int $page Which page in quiz to add the question on. If 0 (default), 1917 * add at the end 1918 * @param float $maxmark The maximum mark to set for this question. (Optional, 1919 * defaults to question.defaultmark. 1920 * @return bool false if the question was already in the quiz 1921 */ 1922 function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null) { 1923 global $DB; 1924 $slots = $DB->get_records('quiz_slots', array('quizid' => $quiz->id), 1925 'slot', 'questionid, slot, page, id'); 1926 if (array_key_exists($questionid, $slots)) { 1927 return false; 1928 } 1929 1930 $trans = $DB->start_delegated_transaction(); 1931 1932 $maxpage = 1; 1933 $numonlastpage = 0; 1934 foreach ($slots as $slot) { 1935 if ($slot->page > $maxpage) { 1936 $maxpage = $slot->page; 1937 $numonlastpage = 1; 1938 } else { 1939 $numonlastpage += 1; 1940 } 1941 } 1942 1943 // Add the new question instance. 1944 $slot = new stdClass(); 1945 $slot->quizid = $quiz->id; 1946 $slot->questionid = $questionid; 1947 1948 if ($maxmark !== null) { 1949 $slot->maxmark = $maxmark; 1950 } else { 1951 $slot->maxmark = $DB->get_field('question', 'defaultmark', array('id' => $questionid)); 1952 } 1953 1954 if (is_int($page) && $page >= 1) { 1955 // Adding on a given page. 1956 $lastslotbefore = 0; 1957 foreach (array_reverse($slots) as $otherslot) { 1958 if ($otherslot->page > $page) { 1959 $DB->set_field('quiz_slots', 'slot', $otherslot->slot + 1, array('id' => $otherslot->id)); 1960 } else { 1961 $lastslotbefore = $otherslot->slot; 1962 break; 1963 } 1964 } 1965 $slot->slot = $lastslotbefore + 1; 1966 $slot->page = min($page, $maxpage + 1); 1967 1968 } else { 1969 $lastslot = end($slots); 1970 if ($lastslot) { 1971 $slot->slot = $lastslot->slot + 1; 1972 } else { 1973 $slot->slot = 1; 1974 } 1975 if ($quiz->questionsperpage && $numonlastpage >= $quiz->questionsperpage) { 1976 $slot->page = $maxpage + 1; 1977 } else { 1978 $slot->page = $maxpage; 1979 } 1980 } 1981 1982 $DB->insert_record('quiz_slots', $slot); 1983 $trans->allow_commit(); 1984 } 1985 1986 /** 1987 * Add a random question to the quiz at a given point. 1988 * @param object $quiz the quiz settings. 1989 * @param int $addonpage the page on which to add the question. 1990 * @param int $categoryid the question category to add the question from. 1991 * @param int $number the number of random questions to add. 1992 * @param bool $includesubcategories whether to include questoins from subcategories. 1993 */ 1994 function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number, 1995 $includesubcategories) { 1996 global $DB; 1997 1998 $category = $DB->get_record('question_categories', array('id' => $categoryid)); 1999 if (!$category) { 2000 print_error('invalidcategoryid', 'error'); 2001 } 2002 2003 $catcontext = context::instance_by_id($category->contextid); 2004 require_capability('moodle/question:useall', $catcontext); 2005 2006 // Find existing random questions in this category that are 2007 // not used by any quiz. 2008 if ($existingquestions = $DB->get_records_sql( 2009 "SELECT q.id, q.qtype FROM {question} q 2010 WHERE qtype = 'random' 2011 AND category = ? 2012 AND " . $DB->sql_compare_text('questiontext') . " = ? 2013 AND NOT EXISTS ( 2014 SELECT * 2015 FROM {quiz_slots} 2016 WHERE questionid = q.id) 2017 ORDER BY id", array($category->id, ($includesubcategories ? '1' : '0')))) { 2018 // Take as many of these as needed. 2019 while (($existingquestion = array_shift($existingquestions)) && $number > 0) { 2020 quiz_add_quiz_question($existingquestion->id, $quiz, $addonpage); 2021 $number -= 1; 2022 } 2023 } 2024 2025 if ($number <= 0) { 2026 return; 2027 } 2028 2029 // More random questions are needed, create them. 2030 for ($i = 0; $i < $number; $i += 1) { 2031 $form = new stdClass(); 2032 $form->questiontext = array('text' => ($includesubcategories ? '1' : '0'), 'format' => 0); 2033 $form->category = $category->id . ',' . $category->contextid; 2034 $form->defaultmark = 1; 2035 $form->hidden = 1; 2036 $form->stamp = make_unique_id_code(); // Set the unique code (not to be changed). 2037 $question = new stdClass(); 2038 $question->qtype = 'random'; 2039 $question = question_bank::get_qtype('random')->save_question($question, $form); 2040 if (!isset($question->id)) { 2041 print_error('cannotinsertrandomquestion', 'quiz'); 2042 } 2043 quiz_add_quiz_question($question->id, $quiz, $addonpage); 2044 } 2045 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |