[ Index ] |
PHP Cross Reference of Unnamed Project |
[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 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 require_once($CFG->libdir . '/eventslib.php'); 32 require_once($CFG->dirroot . '/calendar/lib.php'); 33 34 35 /**#@+ 36 * Option controlling what options are offered on the quiz settings form. 37 */ 38 define('QUIZ_MAX_ATTEMPT_OPTION', 10); 39 define('QUIZ_MAX_QPP_OPTION', 50); 40 define('QUIZ_MAX_DECIMAL_OPTION', 5); 41 define('QUIZ_MAX_Q_DECIMAL_OPTION', 7); 42 /**#@-*/ 43 44 /**#@+ 45 * Options determining how the grades from individual attempts are combined to give 46 * the overall grade for a user 47 */ 48 define('QUIZ_GRADEHIGHEST', '1'); 49 define('QUIZ_GRADEAVERAGE', '2'); 50 define('QUIZ_ATTEMPTFIRST', '3'); 51 define('QUIZ_ATTEMPTLAST', '4'); 52 /**#@-*/ 53 54 /** 55 * @var int If start and end date for the quiz are more than this many seconds apart 56 * they will be represented by two separate events in the calendar 57 */ 58 define('QUIZ_MAX_EVENT_LENGTH', 5*24*60*60); // 5 days. 59 60 /**#@+ 61 * Options for navigation method within quizzes. 62 */ 63 define('QUIZ_NAVMETHOD_FREE', 'free'); 64 define('QUIZ_NAVMETHOD_SEQ', 'sequential'); 65 /**#@-*/ 66 67 /** 68 * Given an object containing all the necessary data, 69 * (defined by the form in mod_form.php) this function 70 * will create a new instance and return the id number 71 * of the new instance. 72 * 73 * @param object $quiz the data that came from the form. 74 * @return mixed the id of the new instance on success, 75 * false or a string error message on failure. 76 */ 77 function quiz_add_instance($quiz) { 78 global $DB; 79 $cmid = $quiz->coursemodule; 80 81 // Process the options from the form. 82 $quiz->created = time(); 83 $result = quiz_process_options($quiz); 84 if ($result && is_string($result)) { 85 return $result; 86 } 87 88 // Try to store it in the database. 89 $quiz->id = $DB->insert_record('quiz', $quiz); 90 91 // Create the first section for this quiz. 92 $DB->insert_record('quiz_sections', array('quizid' => $quiz->id, 93 'firstslot' => 1, 'heading' => '', 'shufflequestions' => 0)); 94 95 // Do the processing required after an add or an update. 96 quiz_after_add_or_update($quiz); 97 98 return $quiz->id; 99 } 100 101 /** 102 * Given an object containing all the necessary data, 103 * (defined by the form in mod_form.php) this function 104 * will update an existing instance with new data. 105 * 106 * @param object $quiz the data that came from the form. 107 * @return mixed true on success, false or a string error message on failure. 108 */ 109 function quiz_update_instance($quiz, $mform) { 110 global $CFG, $DB; 111 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 112 113 // Process the options from the form. 114 $result = quiz_process_options($quiz); 115 if ($result && is_string($result)) { 116 return $result; 117 } 118 119 // Get the current value, so we can see what changed. 120 $oldquiz = $DB->get_record('quiz', array('id' => $quiz->instance)); 121 122 // We need two values from the existing DB record that are not in the form, 123 // in some of the function calls below. 124 $quiz->sumgrades = $oldquiz->sumgrades; 125 $quiz->grade = $oldquiz->grade; 126 127 // Update the database. 128 $quiz->id = $quiz->instance; 129 $DB->update_record('quiz', $quiz); 130 131 // Do the processing required after an add or an update. 132 quiz_after_add_or_update($quiz); 133 134 if ($oldquiz->grademethod != $quiz->grademethod) { 135 quiz_update_all_final_grades($quiz); 136 quiz_update_grades($quiz); 137 } 138 139 $quizdateschanged = $oldquiz->timelimit != $quiz->timelimit 140 || $oldquiz->timeclose != $quiz->timeclose 141 || $oldquiz->graceperiod != $quiz->graceperiod; 142 if ($quizdateschanged) { 143 quiz_update_open_attempts(array('quizid' => $quiz->id)); 144 } 145 146 // Delete any previous preview attempts. 147 quiz_delete_previews($quiz); 148 149 // Repaginate, if asked to. 150 if (!empty($quiz->repaginatenow)) { 151 quiz_repaginate_questions($quiz->id, $quiz->questionsperpage); 152 } 153 154 return true; 155 } 156 157 /** 158 * Given an ID of an instance of this module, 159 * this function will permanently delete the instance 160 * and any data that depends on it. 161 * 162 * @param int $id the id of the quiz to delete. 163 * @return bool success or failure. 164 */ 165 function quiz_delete_instance($id) { 166 global $DB; 167 168 $quiz = $DB->get_record('quiz', array('id' => $id), '*', MUST_EXIST); 169 170 quiz_delete_all_attempts($quiz); 171 quiz_delete_all_overrides($quiz); 172 173 // Look for random questions that may no longer be used when this quiz is gone. 174 $sql = "SELECT q.id 175 FROM {quiz_slots} slot 176 JOIN {question} q ON q.id = slot.questionid 177 WHERE slot.quizid = ? AND q.qtype = ?"; 178 $questionids = $DB->get_fieldset_sql($sql, array($quiz->id, 'random')); 179 180 // We need to do this before we try and delete randoms, otherwise they would still be 'in use'. 181 $DB->delete_records('quiz_slots', array('quizid' => $quiz->id)); 182 $DB->delete_records('quiz_sections', array('quizid' => $quiz->id)); 183 184 foreach ($questionids as $questionid) { 185 question_delete_question($questionid); 186 } 187 188 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id)); 189 190 quiz_access_manager::delete_settings($quiz); 191 192 $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id)); 193 foreach ($events as $event) { 194 $event = calendar_event::load($event); 195 $event->delete(); 196 } 197 198 quiz_grade_item_delete($quiz); 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 * @return bool true on success 210 */ 211 function quiz_delete_override($quiz, $overrideid) { 212 global $DB; 213 214 if (!isset($quiz->cmid)) { 215 $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course); 216 $quiz->cmid = $cm->id; 217 } 218 219 $override = $DB->get_record('quiz_overrides', array('id' => $overrideid), '*', MUST_EXIST); 220 221 // Delete the events. 222 $events = $DB->get_records('event', array('modulename' => 'quiz', 223 'instance' => $quiz->id, 'groupid' => (int)$override->groupid, 224 'userid' => (int)$override->userid)); 225 foreach ($events as $event) { 226 $eventold = calendar_event::load($event); 227 $eventold->delete(); 228 } 229 230 $DB->delete_records('quiz_overrides', array('id' => $overrideid)); 231 232 // Set the common parameters for one of the events we will be triggering. 233 $params = array( 234 'objectid' => $override->id, 235 'context' => context_module::instance($quiz->cmid), 236 'other' => array( 237 'quizid' => $override->quiz 238 ) 239 ); 240 // Determine which override deleted event to fire. 241 if (!empty($override->userid)) { 242 $params['relateduserid'] = $override->userid; 243 $event = \mod_quiz\event\user_override_deleted::create($params); 244 } else { 245 $params['other']['groupid'] = $override->groupid; 246 $event = \mod_quiz\event\group_override_deleted::create($params); 247 } 248 249 // Trigger the override deleted event. 250 $event->add_record_snapshot('quiz_overrides', $override); 251 $event->trigger(); 252 253 return true; 254 } 255 256 /** 257 * Deletes all quiz overrides from the database and clears any corresponding calendar events 258 * 259 * @param object $quiz The quiz object. 260 */ 261 function quiz_delete_all_overrides($quiz) { 262 global $DB; 263 264 $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id'); 265 foreach ($overrides as $override) { 266 quiz_delete_override($quiz, $override->id); 267 } 268 } 269 270 /** 271 * Updates a quiz object with override information for a user. 272 * 273 * Algorithm: For each quiz setting, if there is a matching user-specific override, 274 * then use that otherwise, if there are group-specific overrides, return the most 275 * lenient combination of them. If neither applies, leave the quiz setting unchanged. 276 * 277 * Special case: if there is more than one password that applies to the user, then 278 * quiz->extrapasswords will contain an array of strings giving the remaining 279 * passwords. 280 * 281 * @param object $quiz The quiz object. 282 * @param int $userid The userid. 283 * @return object $quiz The updated quiz object. 284 */ 285 function quiz_update_effective_access($quiz, $userid) { 286 global $DB; 287 288 // Check for user override. 289 $override = $DB->get_record('quiz_overrides', array('quiz' => $quiz->id, 'userid' => $userid)); 290 291 if (!$override) { 292 $override = new stdClass(); 293 $override->timeopen = null; 294 $override->timeclose = null; 295 $override->timelimit = null; 296 $override->attempts = null; 297 $override->password = null; 298 } 299 300 // Check for group overrides. 301 $groupings = groups_get_user_groups($quiz->course, $userid); 302 303 if (!empty($groupings[0])) { 304 // Select all overrides that apply to the User's groups. 305 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0])); 306 $sql = "SELECT * FROM {quiz_overrides} 307 WHERE groupid $extra AND quiz = ?"; 308 $params[] = $quiz->id; 309 $records = $DB->get_records_sql($sql, $params); 310 311 // Combine the overrides. 312 $opens = array(); 313 $closes = array(); 314 $limits = array(); 315 $attempts = array(); 316 $passwords = array(); 317 318 foreach ($records as $gpoverride) { 319 if (isset($gpoverride->timeopen)) { 320 $opens[] = $gpoverride->timeopen; 321 } 322 if (isset($gpoverride->timeclose)) { 323 $closes[] = $gpoverride->timeclose; 324 } 325 if (isset($gpoverride->timelimit)) { 326 $limits[] = $gpoverride->timelimit; 327 } 328 if (isset($gpoverride->attempts)) { 329 $attempts[] = $gpoverride->attempts; 330 } 331 if (isset($gpoverride->password)) { 332 $passwords[] = $gpoverride->password; 333 } 334 } 335 // If there is a user override for a setting, ignore the group override. 336 if (is_null($override->timeopen) && count($opens)) { 337 $override->timeopen = min($opens); 338 } 339 if (is_null($override->timeclose) && count($closes)) { 340 if (in_array(0, $closes)) { 341 $override->timeclose = 0; 342 } else { 343 $override->timeclose = max($closes); 344 } 345 } 346 if (is_null($override->timelimit) && count($limits)) { 347 if (in_array(0, $limits)) { 348 $override->timelimit = 0; 349 } else { 350 $override->timelimit = max($limits); 351 } 352 } 353 if (is_null($override->attempts) && count($attempts)) { 354 if (in_array(0, $attempts)) { 355 $override->attempts = 0; 356 } else { 357 $override->attempts = max($attempts); 358 } 359 } 360 if (is_null($override->password) && count($passwords)) { 361 $override->password = array_shift($passwords); 362 if (count($passwords)) { 363 $override->extrapasswords = $passwords; 364 } 365 } 366 367 } 368 369 // Merge with quiz defaults. 370 $keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password', 'extrapasswords'); 371 foreach ($keys as $key) { 372 if (isset($override->{$key})) { 373 $quiz->{$key} = $override->{$key}; 374 } 375 } 376 377 return $quiz; 378 } 379 380 /** 381 * Delete all the attempts belonging to a quiz. 382 * 383 * @param object $quiz The quiz object. 384 */ 385 function quiz_delete_all_attempts($quiz) { 386 global $CFG, $DB; 387 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 388 question_engine::delete_questions_usage_by_activities(new qubaids_for_quiz($quiz->id)); 389 $DB->delete_records('quiz_attempts', array('quiz' => $quiz->id)); 390 $DB->delete_records('quiz_grades', array('quiz' => $quiz->id)); 391 } 392 393 /** 394 * Get the best current grade for a particular user in a quiz. 395 * 396 * @param object $quiz the quiz settings. 397 * @param int $userid the id of the user. 398 * @return float the user's current grade for this quiz, or null if this user does 399 * not have a grade on this quiz. 400 */ 401 function quiz_get_best_grade($quiz, $userid) { 402 global $DB; 403 $grade = $DB->get_field('quiz_grades', 'grade', 404 array('quiz' => $quiz->id, 'userid' => $userid)); 405 406 // Need to detect errors/no result, without catching 0 grades. 407 if ($grade === false) { 408 return null; 409 } 410 411 return $grade + 0; // Convert to number. 412 } 413 414 /** 415 * Is this a graded quiz? If this method returns true, you can assume that 416 * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to 417 * divide by them). 418 * 419 * @param object $quiz a row from the quiz table. 420 * @return bool whether this is a graded quiz. 421 */ 422 function quiz_has_grades($quiz) { 423 return $quiz->grade >= 0.000005 && $quiz->sumgrades >= 0.000005; 424 } 425 426 /** 427 * Does this quiz allow multiple tries? 428 * 429 * @return bool 430 */ 431 function quiz_allows_multiple_tries($quiz) { 432 $bt = question_engine::get_behaviour_type($quiz->preferredbehaviour); 433 return $bt->allows_multiple_submitted_responses(); 434 } 435 436 /** 437 * Return a small object with summary information about what a 438 * user has done with a given particular instance of this module 439 * Used for user activity reports. 440 * $return->time = the time they did it 441 * $return->info = a short text description 442 * 443 * @param object $course 444 * @param object $user 445 * @param object $mod 446 * @param object $quiz 447 * @return object|null 448 */ 449 function quiz_user_outline($course, $user, $mod, $quiz) { 450 global $DB, $CFG; 451 require_once($CFG->libdir . '/gradelib.php'); 452 $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id); 453 454 if (empty($grades->items[0]->grades)) { 455 return null; 456 } else { 457 $grade = reset($grades->items[0]->grades); 458 } 459 460 $result = new stdClass(); 461 $result->info = get_string('grade') . ': ' . $grade->str_long_grade; 462 463 // Datesubmitted == time created. dategraded == time modified or time overridden 464 // if grade was last modified by the user themselves use date graded. Otherwise use 465 // date submitted. 466 // TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704. 467 if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) { 468 $result->time = $grade->dategraded; 469 } else { 470 $result->time = $grade->datesubmitted; 471 } 472 473 return $result; 474 } 475 476 /** 477 * Print a detailed representation of what a user has done with 478 * a given particular instance of this module, for user activity reports. 479 * 480 * @param object $course 481 * @param object $user 482 * @param object $mod 483 * @param object $quiz 484 * @return bool 485 */ 486 function quiz_user_complete($course, $user, $mod, $quiz) { 487 global $DB, $CFG, $OUTPUT; 488 require_once($CFG->libdir . '/gradelib.php'); 489 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 490 491 $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id); 492 if (!empty($grades->items[0]->grades)) { 493 $grade = reset($grades->items[0]->grades); 494 echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade); 495 if ($grade->str_feedback) { 496 echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback); 497 } 498 } 499 500 if ($attempts = $DB->get_records('quiz_attempts', 501 array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) { 502 foreach ($attempts as $attempt) { 503 echo get_string('attempt', 'quiz', $attempt->attempt) . ': '; 504 if ($attempt->state != quiz_attempt::FINISHED) { 505 echo quiz_attempt_state_name($attempt->state); 506 } else { 507 echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' . 508 quiz_format_grade($quiz, $quiz->sumgrades); 509 } 510 echo ' - '.userdate($attempt->timemodified).'<br />'; 511 } 512 } else { 513 print_string('noattempts', 'quiz'); 514 } 515 516 return true; 517 } 518 519 /** 520 * Quiz periodic clean-up tasks. 521 */ 522 function quiz_cron() { 523 global $CFG; 524 525 require_once($CFG->dirroot . '/mod/quiz/cronlib.php'); 526 mtrace(''); 527 528 $timenow = time(); 529 $overduehander = new mod_quiz_overdue_attempt_updater(); 530 531 $processto = $timenow - get_config('quiz', 'graceperiodmin'); 532 533 mtrace(' Looking for quiz overdue quiz attempts...'); 534 535 list($count, $quizcount) = $overduehander->update_overdue_attempts($timenow, $processto); 536 537 mtrace(' Considered ' . $count . ' attempts in ' . $quizcount . ' quizzes.'); 538 539 // Run cron for our sub-plugin types. 540 cron_execute_plugin_type('quiz', 'quiz reports'); 541 cron_execute_plugin_type('quizaccess', 'quiz access rules'); 542 543 return true; 544 } 545 546 /** 547 * @param int|array $quizids A quiz ID, or an array of quiz IDs. 548 * @param int $userid the userid. 549 * @param string $status 'all', 'finished' or 'unfinished' to control 550 * @param bool $includepreviews 551 * @return an array of all the user's attempts at this quiz. Returns an empty 552 * array if there are none. 553 */ 554 function quiz_get_user_attempts($quizids, $userid, $status = 'finished', $includepreviews = false) { 555 global $DB, $CFG; 556 // TODO MDL-33071 it is very annoying to have to included all of locallib.php 557 // just to get the quiz_attempt::FINISHED constants, but I will try to sort 558 // that out properly for Moodle 2.4. For now, I will just do a quick fix for 559 // MDL-33048. 560 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 561 562 $params = array(); 563 switch ($status) { 564 case 'all': 565 $statuscondition = ''; 566 break; 567 568 case 'finished': 569 $statuscondition = ' AND state IN (:state1, :state2)'; 570 $params['state1'] = quiz_attempt::FINISHED; 571 $params['state2'] = quiz_attempt::ABANDONED; 572 break; 573 574 case 'unfinished': 575 $statuscondition = ' AND state IN (:state1, :state2)'; 576 $params['state1'] = quiz_attempt::IN_PROGRESS; 577 $params['state2'] = quiz_attempt::OVERDUE; 578 break; 579 } 580 581 $quizids = (array) $quizids; 582 list($insql, $inparams) = $DB->get_in_or_equal($quizids, SQL_PARAMS_NAMED); 583 $params += $inparams; 584 $params['userid'] = $userid; 585 586 $previewclause = ''; 587 if (!$includepreviews) { 588 $previewclause = ' AND preview = 0'; 589 } 590 591 return $DB->get_records_select('quiz_attempts', 592 "quiz $insql AND userid = :userid" . $previewclause . $statuscondition, 593 $params, 'quiz, attempt ASC'); 594 } 595 596 /** 597 * Return grade for given user or all users. 598 * 599 * @param int $quizid id of quiz 600 * @param int $userid optional user id, 0 means all users 601 * @return array array of grades, false if none. These are raw grades. They should 602 * be processed with quiz_format_grade for display. 603 */ 604 function quiz_get_user_grades($quiz, $userid = 0) { 605 global $CFG, $DB; 606 607 $params = array($quiz->id); 608 $usertest = ''; 609 if ($userid) { 610 $params[] = $userid; 611 $usertest = 'AND u.id = ?'; 612 } 613 return $DB->get_records_sql(" 614 SELECT 615 u.id, 616 u.id AS userid, 617 qg.grade AS rawgrade, 618 qg.timemodified AS dategraded, 619 MAX(qa.timefinish) AS datesubmitted 620 621 FROM {user} u 622 JOIN {quiz_grades} qg ON u.id = qg.userid 623 JOIN {quiz_attempts} qa ON qa.quiz = qg.quiz AND qa.userid = u.id 624 625 WHERE qg.quiz = ? 626 $usertest 627 GROUP BY u.id, qg.grade, qg.timemodified", $params); 628 } 629 630 /** 631 * Round a grade to to the correct number of decimal places, and format it for display. 632 * 633 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used. 634 * @param float $grade The grade to round. 635 * @return float 636 */ 637 function quiz_format_grade($quiz, $grade) { 638 if (is_null($grade)) { 639 return get_string('notyetgraded', 'quiz'); 640 } 641 return format_float($grade, $quiz->decimalpoints); 642 } 643 644 /** 645 * Determine the correct number of decimal places required to format a grade. 646 * 647 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used. 648 * @return integer 649 */ 650 function quiz_get_grade_format($quiz) { 651 if (empty($quiz->questiondecimalpoints)) { 652 $quiz->questiondecimalpoints = -1; 653 } 654 655 if ($quiz->questiondecimalpoints == -1) { 656 return $quiz->decimalpoints; 657 } 658 659 return $quiz->questiondecimalpoints; 660 } 661 662 /** 663 * Round a grade to the correct number of decimal places, and format it for display. 664 * 665 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used. 666 * @param float $grade The grade to round. 667 * @return float 668 */ 669 function quiz_format_question_grade($quiz, $grade) { 670 return format_float($grade, quiz_get_grade_format($quiz)); 671 } 672 673 /** 674 * Update grades in central gradebook 675 * 676 * @category grade 677 * @param object $quiz the quiz settings. 678 * @param int $userid specific user only, 0 means all users. 679 * @param bool $nullifnone If a single user is specified and $nullifnone is true a grade item with a null rawgrade will be inserted 680 */ 681 function quiz_update_grades($quiz, $userid = 0, $nullifnone = true) { 682 global $CFG, $DB; 683 require_once($CFG->libdir . '/gradelib.php'); 684 685 if ($quiz->grade == 0) { 686 quiz_grade_item_update($quiz); 687 688 } else if ($grades = quiz_get_user_grades($quiz, $userid)) { 689 quiz_grade_item_update($quiz, $grades); 690 691 } else if ($userid && $nullifnone) { 692 $grade = new stdClass(); 693 $grade->userid = $userid; 694 $grade->rawgrade = null; 695 quiz_grade_item_update($quiz, $grade); 696 697 } else { 698 quiz_grade_item_update($quiz); 699 } 700 } 701 702 /** 703 * Create or update the grade item for given quiz 704 * 705 * @category grade 706 * @param object $quiz object with extra cmidnumber 707 * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook 708 * @return int 0 if ok, error code otherwise 709 */ 710 function quiz_grade_item_update($quiz, $grades = null) { 711 global $CFG, $OUTPUT; 712 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 713 require_once($CFG->libdir . '/gradelib.php'); 714 715 if (array_key_exists('cmidnumber', $quiz)) { // May not be always present. 716 $params = array('itemname' => $quiz->name, 'idnumber' => $quiz->cmidnumber); 717 } else { 718 $params = array('itemname' => $quiz->name); 719 } 720 721 if ($quiz->grade > 0) { 722 $params['gradetype'] = GRADE_TYPE_VALUE; 723 $params['grademax'] = $quiz->grade; 724 $params['grademin'] = 0; 725 726 } else { 727 $params['gradetype'] = GRADE_TYPE_NONE; 728 } 729 730 // What this is trying to do: 731 // 1. If the quiz is set to not show grades while the quiz is still open, 732 // and is set to show grades after the quiz is closed, then create the 733 // grade_item with a show-after date that is the quiz close date. 734 // 2. If the quiz is set to not show grades at either of those times, 735 // create the grade_item as hidden. 736 // 3. If the quiz is set to show grades, create the grade_item visible. 737 $openreviewoptions = mod_quiz_display_options::make_from_quiz($quiz, 738 mod_quiz_display_options::LATER_WHILE_OPEN); 739 $closedreviewoptions = mod_quiz_display_options::make_from_quiz($quiz, 740 mod_quiz_display_options::AFTER_CLOSE); 741 if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX && 742 $closedreviewoptions->marks < question_display_options::MARK_AND_MAX) { 743 $params['hidden'] = 1; 744 745 } else if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX && 746 $closedreviewoptions->marks >= question_display_options::MARK_AND_MAX) { 747 if ($quiz->timeclose) { 748 $params['hidden'] = $quiz->timeclose; 749 } else { 750 $params['hidden'] = 1; 751 } 752 753 } else { 754 // Either 755 // a) both open and closed enabled 756 // b) open enabled, closed disabled - we can not "hide after", 757 // grades are kept visible even after closing. 758 $params['hidden'] = 0; 759 } 760 761 if (!$params['hidden']) { 762 // If the grade item is not hidden by the quiz logic, then we need to 763 // hide it if the quiz is hidden from students. 764 if (property_exists($quiz, 'visible')) { 765 // Saving the quiz form, and cm not yet updated in the database. 766 $params['hidden'] = !$quiz->visible; 767 } else { 768 $cm = get_coursemodule_from_instance('quiz', $quiz->id); 769 $params['hidden'] = !$cm->visible; 770 } 771 } 772 773 if ($grades === 'reset') { 774 $params['reset'] = true; 775 $grades = null; 776 } 777 778 $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id); 779 if (!empty($gradebook_grades->items)) { 780 $grade_item = $gradebook_grades->items[0]; 781 if ($grade_item->locked) { 782 // NOTE: this is an extremely nasty hack! It is not a bug if this confirmation fails badly. --skodak. 783 $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT); 784 if (!$confirm_regrade) { 785 if (!AJAX_SCRIPT) { 786 $message = get_string('gradeitemislocked', 'grades'); 787 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id . 788 '&mode=overview'; 789 $regrade_link = qualified_me() . '&confirm_regrade=1'; 790 echo $OUTPUT->box_start('generalbox', 'notice'); 791 echo '<p>'. $message .'</p>'; 792 echo $OUTPUT->container_start('buttons'); 793 echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades')); 794 echo $OUTPUT->single_button($back_link, get_string('cancel')); 795 echo $OUTPUT->container_end(); 796 echo $OUTPUT->box_end(); 797 } 798 return GRADE_UPDATE_ITEM_LOCKED; 799 } 800 } 801 } 802 803 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params); 804 } 805 806 /** 807 * Delete grade item for given quiz 808 * 809 * @category grade 810 * @param object $quiz object 811 * @return object quiz 812 */ 813 function quiz_grade_item_delete($quiz) { 814 global $CFG; 815 require_once($CFG->libdir . '/gradelib.php'); 816 817 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, 818 null, array('deleted' => 1)); 819 } 820 821 /** 822 * This standard function will check all instances of this module 823 * and make sure there are up-to-date events created for each of them. 824 * If courseid = 0, then every quiz event in the site is checked, else 825 * only quiz events belonging to the course specified are checked. 826 * This function is used, in its new format, by restore_refresh_events() 827 * 828 * @param int $courseid 829 * @return bool 830 */ 831 function quiz_refresh_events($courseid = 0) { 832 global $DB; 833 834 if ($courseid == 0) { 835 if (!$quizzes = $DB->get_records('quiz')) { 836 return true; 837 } 838 } else { 839 if (!$quizzes = $DB->get_records('quiz', array('course' => $courseid))) { 840 return true; 841 } 842 } 843 844 foreach ($quizzes as $quiz) { 845 quiz_update_events($quiz); 846 } 847 848 return true; 849 } 850 851 /** 852 * Returns all quiz graded users since a given time for specified quiz 853 */ 854 function quiz_get_recent_mod_activity(&$activities, &$index, $timestart, 855 $courseid, $cmid, $userid = 0, $groupid = 0) { 856 global $CFG, $USER, $DB; 857 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 858 859 $course = get_course($courseid); 860 $modinfo = get_fast_modinfo($course); 861 862 $cm = $modinfo->cms[$cmid]; 863 $quiz = $DB->get_record('quiz', array('id' => $cm->instance)); 864 865 if ($userid) { 866 $userselect = "AND u.id = :userid"; 867 $params['userid'] = $userid; 868 } else { 869 $userselect = ''; 870 } 871 872 if ($groupid) { 873 $groupselect = 'AND gm.groupid = :groupid'; 874 $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id'; 875 $params['groupid'] = $groupid; 876 } else { 877 $groupselect = ''; 878 $groupjoin = ''; 879 } 880 881 $params['timestart'] = $timestart; 882 $params['quizid'] = $quiz->id; 883 884 $ufields = user_picture::fields('u', null, 'useridagain'); 885 if (!$attempts = $DB->get_records_sql(" 886 SELECT qa.*, 887 {$ufields} 888 FROM {quiz_attempts} qa 889 JOIN {user} u ON u.id = qa.userid 890 $groupjoin 891 WHERE qa.timefinish > :timestart 892 AND qa.quiz = :quizid 893 AND qa.preview = 0 894 $userselect 895 $groupselect 896 ORDER BY qa.timefinish ASC", $params)) { 897 return; 898 } 899 900 $context = context_module::instance($cm->id); 901 $accessallgroups = has_capability('moodle/site:accessallgroups', $context); 902 $viewfullnames = has_capability('moodle/site:viewfullnames', $context); 903 $grader = has_capability('mod/quiz:viewreports', $context); 904 $groupmode = groups_get_activity_groupmode($cm, $course); 905 906 $usersgroups = null; 907 $aname = format_string($cm->name, true); 908 foreach ($attempts as $attempt) { 909 if ($attempt->userid != $USER->id) { 910 if (!$grader) { 911 // Grade permission required. 912 continue; 913 } 914 915 if ($groupmode == SEPARATEGROUPS and !$accessallgroups) { 916 $usersgroups = groups_get_all_groups($course->id, 917 $attempt->userid, $cm->groupingid); 918 $usersgroups = array_keys($usersgroups); 919 if (!array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid))) { 920 continue; 921 } 922 } 923 } 924 925 $options = quiz_get_review_options($quiz, $attempt, $context); 926 927 $tmpactivity = new stdClass(); 928 929 $tmpactivity->type = 'quiz'; 930 $tmpactivity->cmid = $cm->id; 931 $tmpactivity->name = $aname; 932 $tmpactivity->sectionnum = $cm->sectionnum; 933 $tmpactivity->timestamp = $attempt->timefinish; 934 935 $tmpactivity->content = new stdClass(); 936 $tmpactivity->content->attemptid = $attempt->id; 937 $tmpactivity->content->attempt = $attempt->attempt; 938 if (quiz_has_grades($quiz) && $options->marks >= question_display_options::MARK_AND_MAX) { 939 $tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades); 940 $tmpactivity->content->maxgrade = quiz_format_grade($quiz, $quiz->sumgrades); 941 } else { 942 $tmpactivity->content->sumgrades = null; 943 $tmpactivity->content->maxgrade = null; 944 } 945 946 $tmpactivity->user = user_picture::unalias($attempt, null, 'useridagain'); 947 $tmpactivity->user->fullname = fullname($tmpactivity->user, $viewfullnames); 948 949 $activities[$index++] = $tmpactivity; 950 } 951 } 952 953 function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) { 954 global $CFG, $OUTPUT; 955 956 echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">'; 957 958 echo '<tr><td class="userpicture" valign="top">'; 959 echo $OUTPUT->user_picture($activity->user, array('courseid' => $courseid)); 960 echo '</td><td>'; 961 962 if ($detail) { 963 $modname = $modnames[$activity->type]; 964 echo '<div class="title">'; 965 echo '<img src="' . $OUTPUT->pix_url('icon', $activity->type) . '" ' . 966 'class="icon" alt="' . $modname . '" />'; 967 echo '<a href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' . 968 $activity->cmid . '">' . $activity->name . '</a>'; 969 echo '</div>'; 970 } 971 972 echo '<div class="grade">'; 973 echo get_string('attempt', 'quiz', $activity->content->attempt); 974 if (isset($activity->content->maxgrade)) { 975 $grades = $activity->content->sumgrades . ' / ' . $activity->content->maxgrade; 976 echo ': (<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . 977 $activity->content->attemptid . '">' . $grades . '</a>)'; 978 } 979 echo '</div>'; 980 981 echo '<div class="user">'; 982 echo '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $activity->user->id . 983 '&course=' . $courseid . '">' . $activity->user->fullname . 984 '</a> - ' . userdate($activity->timestamp); 985 echo '</div>'; 986 987 echo '</td></tr></table>'; 988 989 return; 990 } 991 992 /** 993 * Pre-process the quiz options form data, making any necessary adjustments. 994 * Called by add/update instance in this file. 995 * 996 * @param object $quiz The variables set on the form. 997 */ 998 function quiz_process_options($quiz) { 999 global $CFG; 1000 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 1001 require_once($CFG->libdir . '/questionlib.php'); 1002 1003 $quiz->timemodified = time(); 1004 1005 // Quiz name. 1006 if (!empty($quiz->name)) { 1007 $quiz->name = trim($quiz->name); 1008 } 1009 1010 // Password field - different in form to stop browsers that remember passwords 1011 // getting confused. 1012 $quiz->password = $quiz->quizpassword; 1013 unset($quiz->quizpassword); 1014 1015 // Quiz feedback. 1016 if (isset($quiz->feedbacktext)) { 1017 // Clean up the boundary text. 1018 for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) { 1019 if (empty($quiz->feedbacktext[$i]['text'])) { 1020 $quiz->feedbacktext[$i]['text'] = ''; 1021 } else { 1022 $quiz->feedbacktext[$i]['text'] = trim($quiz->feedbacktext[$i]['text']); 1023 } 1024 } 1025 1026 // Check the boundary value is a number or a percentage, and in range. 1027 $i = 0; 1028 while (!empty($quiz->feedbackboundaries[$i])) { 1029 $boundary = trim($quiz->feedbackboundaries[$i]); 1030 if (!is_numeric($boundary)) { 1031 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') { 1032 $boundary = trim(substr($boundary, 0, -1)); 1033 if (is_numeric($boundary)) { 1034 $boundary = $boundary * $quiz->grade / 100.0; 1035 } else { 1036 return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1); 1037 } 1038 } 1039 } 1040 if ($boundary <= 0 || $boundary >= $quiz->grade) { 1041 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1); 1042 } 1043 if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) { 1044 return get_string('feedbackerrororder', 'quiz', $i + 1); 1045 } 1046 $quiz->feedbackboundaries[$i] = $boundary; 1047 $i += 1; 1048 } 1049 $numboundaries = $i; 1050 1051 // Check there is nothing in the remaining unused fields. 1052 if (!empty($quiz->feedbackboundaries)) { 1053 for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) { 1054 if (!empty($quiz->feedbackboundaries[$i]) && 1055 trim($quiz->feedbackboundaries[$i]) != '') { 1056 return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1); 1057 } 1058 } 1059 } 1060 for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) { 1061 if (!empty($quiz->feedbacktext[$i]['text']) && 1062 trim($quiz->feedbacktext[$i]['text']) != '') { 1063 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1); 1064 } 1065 } 1066 // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade(). 1067 $quiz->feedbackboundaries[-1] = $quiz->grade + 1; 1068 $quiz->feedbackboundaries[$numboundaries] = 0; 1069 $quiz->feedbackboundarycount = $numboundaries; 1070 } else { 1071 $quiz->feedbackboundarycount = -1; 1072 } 1073 1074 // Combing the individual settings into the review columns. 1075 $quiz->reviewattempt = quiz_review_option_form_to_db($quiz, 'attempt'); 1076 $quiz->reviewcorrectness = quiz_review_option_form_to_db($quiz, 'correctness'); 1077 $quiz->reviewmarks = quiz_review_option_form_to_db($quiz, 'marks'); 1078 $quiz->reviewspecificfeedback = quiz_review_option_form_to_db($quiz, 'specificfeedback'); 1079 $quiz->reviewgeneralfeedback = quiz_review_option_form_to_db($quiz, 'generalfeedback'); 1080 $quiz->reviewrightanswer = quiz_review_option_form_to_db($quiz, 'rightanswer'); 1081 $quiz->reviewoverallfeedback = quiz_review_option_form_to_db($quiz, 'overallfeedback'); 1082 $quiz->reviewattempt |= mod_quiz_display_options::DURING; 1083 $quiz->reviewoverallfeedback &= ~mod_quiz_display_options::DURING; 1084 } 1085 1086 /** 1087 * Helper function for {@link quiz_process_options()}. 1088 * @param object $fromform the sumbitted form date. 1089 * @param string $field one of the review option field names. 1090 */ 1091 function quiz_review_option_form_to_db($fromform, $field) { 1092 static $times = array( 1093 'during' => mod_quiz_display_options::DURING, 1094 'immediately' => mod_quiz_display_options::IMMEDIATELY_AFTER, 1095 'open' => mod_quiz_display_options::LATER_WHILE_OPEN, 1096 'closed' => mod_quiz_display_options::AFTER_CLOSE, 1097 ); 1098 1099 $review = 0; 1100 foreach ($times as $whenname => $when) { 1101 $fieldname = $field . $whenname; 1102 if (isset($fromform->$fieldname)) { 1103 $review |= $when; 1104 unset($fromform->$fieldname); 1105 } 1106 } 1107 1108 return $review; 1109 } 1110 1111 /** 1112 * This function is called at the end of quiz_add_instance 1113 * and quiz_update_instance, to do the common processing. 1114 * 1115 * @param object $quiz the quiz object. 1116 */ 1117 function quiz_after_add_or_update($quiz) { 1118 global $DB; 1119 $cmid = $quiz->coursemodule; 1120 1121 // We need to use context now, so we need to make sure all needed info is already in db. 1122 $DB->set_field('course_modules', 'instance', $quiz->id, array('id'=>$cmid)); 1123 $context = context_module::instance($cmid); 1124 1125 // Save the feedback. 1126 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id)); 1127 1128 for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) { 1129 $feedback = new stdClass(); 1130 $feedback->quizid = $quiz->id; 1131 $feedback->feedbacktext = $quiz->feedbacktext[$i]['text']; 1132 $feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format']; 1133 $feedback->mingrade = $quiz->feedbackboundaries[$i]; 1134 $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1]; 1135 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1136 $feedbacktext = file_save_draft_area_files((int)$quiz->feedbacktext[$i]['itemid'], 1137 $context->id, 'mod_quiz', 'feedback', $feedback->id, 1138 array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0), 1139 $quiz->feedbacktext[$i]['text']); 1140 $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext, 1141 array('id' => $feedback->id)); 1142 } 1143 1144 // Store any settings belonging to the access rules. 1145 quiz_access_manager::save_settings($quiz); 1146 1147 // Update the events relating to this quiz. 1148 quiz_update_events($quiz); 1149 1150 // Update related grade item. 1151 quiz_grade_item_update($quiz); 1152 } 1153 1154 /** 1155 * This function updates the events associated to the quiz. 1156 * If $override is non-zero, then it updates only the events 1157 * associated with the specified override. 1158 * 1159 * @uses QUIZ_MAX_EVENT_LENGTH 1160 * @param object $quiz the quiz object. 1161 * @param object optional $override limit to a specific override 1162 */ 1163 function quiz_update_events($quiz, $override = null) { 1164 global $DB; 1165 1166 // Load the old events relating to this quiz. 1167 $conds = array('modulename'=>'quiz', 1168 'instance'=>$quiz->id); 1169 if (!empty($override)) { 1170 // Only load events for this override. 1171 $conds['groupid'] = isset($override->groupid)? $override->groupid : 0; 1172 $conds['userid'] = isset($override->userid)? $override->userid : 0; 1173 } 1174 $oldevents = $DB->get_records('event', $conds); 1175 1176 // Now make a todo list of all that needs to be updated. 1177 if (empty($override)) { 1178 // We are updating the primary settings for the quiz, so we 1179 // need to add all the overrides. 1180 $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id)); 1181 // As well as the original quiz (empty override). 1182 $overrides[] = new stdClass(); 1183 } else { 1184 // Just do the one override. 1185 $overrides = array($override); 1186 } 1187 1188 foreach ($overrides as $current) { 1189 $groupid = isset($current->groupid)? $current->groupid : 0; 1190 $userid = isset($current->userid)? $current->userid : 0; 1191 $timeopen = isset($current->timeopen)? $current->timeopen : $quiz->timeopen; 1192 $timeclose = isset($current->timeclose)? $current->timeclose : $quiz->timeclose; 1193 1194 // Only add open/close events for an override if they differ from the quiz default. 1195 $addopen = empty($current->id) || !empty($current->timeopen); 1196 $addclose = empty($current->id) || !empty($current->timeclose); 1197 1198 if (!empty($quiz->coursemodule)) { 1199 $cmid = $quiz->coursemodule; 1200 } else { 1201 $cmid = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course)->id; 1202 } 1203 1204 $event = new stdClass(); 1205 $event->description = format_module_intro('quiz', $quiz, $cmid); 1206 // Events module won't show user events when the courseid is nonzero. 1207 $event->courseid = ($userid) ? 0 : $quiz->course; 1208 $event->groupid = $groupid; 1209 $event->userid = $userid; 1210 $event->modulename = 'quiz'; 1211 $event->instance = $quiz->id; 1212 $event->timestart = $timeopen; 1213 $event->timeduration = max($timeclose - $timeopen, 0); 1214 $event->visible = instance_is_visible('quiz', $quiz); 1215 $event->eventtype = 'open'; 1216 1217 // Determine the event name. 1218 if ($groupid) { 1219 $params = new stdClass(); 1220 $params->quiz = $quiz->name; 1221 $params->group = groups_get_group_name($groupid); 1222 if ($params->group === false) { 1223 // Group doesn't exist, just skip it. 1224 continue; 1225 } 1226 $eventname = get_string('overridegroupeventname', 'quiz', $params); 1227 } else if ($userid) { 1228 $params = new stdClass(); 1229 $params->quiz = $quiz->name; 1230 $eventname = get_string('overrideusereventname', 'quiz', $params); 1231 } else { 1232 $eventname = $quiz->name; 1233 } 1234 if ($addopen or $addclose) { 1235 if ($timeclose and $timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) { 1236 // Single event for the whole quiz. 1237 if ($oldevent = array_shift($oldevents)) { 1238 $event->id = $oldevent->id; 1239 } else { 1240 unset($event->id); 1241 } 1242 $event->name = $eventname; 1243 // The method calendar_event::create will reuse a db record if the id field is set. 1244 calendar_event::create($event); 1245 } else { 1246 // Separate start and end events. 1247 $event->timeduration = 0; 1248 if ($timeopen && $addopen) { 1249 if ($oldevent = array_shift($oldevents)) { 1250 $event->id = $oldevent->id; 1251 } else { 1252 unset($event->id); 1253 } 1254 $event->name = $eventname.' ('.get_string('quizopens', 'quiz').')'; 1255 // The method calendar_event::create will reuse a db record if the id field is set. 1256 calendar_event::create($event); 1257 } 1258 if ($timeclose && $addclose) { 1259 if ($oldevent = array_shift($oldevents)) { 1260 $event->id = $oldevent->id; 1261 } else { 1262 unset($event->id); 1263 } 1264 $event->name = $eventname.' ('.get_string('quizcloses', 'quiz').')'; 1265 $event->timestart = $timeclose; 1266 $event->eventtype = 'close'; 1267 calendar_event::create($event); 1268 } 1269 } 1270 } 1271 } 1272 1273 // Delete any leftover events. 1274 foreach ($oldevents as $badevent) { 1275 $badevent = calendar_event::load($badevent); 1276 $badevent->delete(); 1277 } 1278 } 1279 1280 /** 1281 * List the actions that correspond to a view of this module. 1282 * This is used by the participation report. 1283 * 1284 * Note: This is not used by new logging system. Event with 1285 * crud = 'r' and edulevel = LEVEL_PARTICIPATING will 1286 * be considered as view action. 1287 * 1288 * @return array 1289 */ 1290 function quiz_get_view_actions() { 1291 return array('view', 'view all', 'report', 'review'); 1292 } 1293 1294 /** 1295 * List the actions that correspond to a post of this module. 1296 * This is used by the participation report. 1297 * 1298 * Note: This is not used by new logging system. Event with 1299 * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING 1300 * will be considered as post action. 1301 * 1302 * @return array 1303 */ 1304 function quiz_get_post_actions() { 1305 return array('attempt', 'close attempt', 'preview', 'editquestions', 1306 'delete attempt', 'manualgrade'); 1307 } 1308 1309 /** 1310 * @param array $questionids of question ids. 1311 * @return bool whether any of these questions are used by any instance of this module. 1312 */ 1313 function quiz_questions_in_use($questionids) { 1314 global $DB, $CFG; 1315 require_once($CFG->libdir . '/questionlib.php'); 1316 list($test, $params) = $DB->get_in_or_equal($questionids); 1317 return $DB->record_exists_select('quiz_slots', 1318 'questionid ' . $test, $params) || question_engine::questions_in_use( 1319 $questionids, new qubaid_join('{quiz_attempts} quiza', 1320 'quiza.uniqueid', 'quiza.preview = 0')); 1321 } 1322 1323 /** 1324 * Implementation of the function for printing the form elements that control 1325 * whether the course reset functionality affects the quiz. 1326 * 1327 * @param $mform the course reset form that is being built. 1328 */ 1329 function quiz_reset_course_form_definition($mform) { 1330 $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz')); 1331 $mform->addElement('advcheckbox', 'reset_quiz_attempts', 1332 get_string('removeallquizattempts', 'quiz')); 1333 $mform->addElement('advcheckbox', 'reset_quiz_user_overrides', 1334 get_string('removealluseroverrides', 'quiz')); 1335 $mform->addElement('advcheckbox', 'reset_quiz_group_overrides', 1336 get_string('removeallgroupoverrides', 'quiz')); 1337 } 1338 1339 /** 1340 * Course reset form defaults. 1341 * @return array the defaults. 1342 */ 1343 function quiz_reset_course_form_defaults($course) { 1344 return array('reset_quiz_attempts' => 1, 1345 'reset_quiz_group_overrides' => 1, 1346 'reset_quiz_user_overrides' => 1); 1347 } 1348 1349 /** 1350 * Removes all grades from gradebook 1351 * 1352 * @param int $courseid 1353 * @param string optional type 1354 */ 1355 function quiz_reset_gradebook($courseid, $type='') { 1356 global $CFG, $DB; 1357 1358 $quizzes = $DB->get_records_sql(" 1359 SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid 1360 FROM {modules} m 1361 JOIN {course_modules} cm ON m.id = cm.module 1362 JOIN {quiz} q ON cm.instance = q.id 1363 WHERE m.name = 'quiz' AND cm.course = ?", array($courseid)); 1364 1365 foreach ($quizzes as $quiz) { 1366 quiz_grade_item_update($quiz, 'reset'); 1367 } 1368 } 1369 1370 /** 1371 * Actual implementation of the reset course functionality, delete all the 1372 * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is 1373 * set and true. 1374 * 1375 * Also, move the quiz open and close dates, if the course start date is changing. 1376 * 1377 * @param object $data the data submitted from the reset course. 1378 * @return array status array 1379 */ 1380 function quiz_reset_userdata($data) { 1381 global $CFG, $DB; 1382 require_once($CFG->libdir . '/questionlib.php'); 1383 1384 $componentstr = get_string('modulenameplural', 'quiz'); 1385 $status = array(); 1386 1387 // Delete attempts. 1388 if (!empty($data->reset_quiz_attempts)) { 1389 question_engine::delete_questions_usage_by_activities(new qubaid_join( 1390 '{quiz_attempts} quiza JOIN {quiz} quiz ON quiza.quiz = quiz.id', 1391 'quiza.uniqueid', 'quiz.course = :quizcourseid', 1392 array('quizcourseid' => $data->courseid))); 1393 1394 $DB->delete_records_select('quiz_attempts', 1395 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid)); 1396 $status[] = array( 1397 'component' => $componentstr, 1398 'item' => get_string('attemptsdeleted', 'quiz'), 1399 'error' => false); 1400 1401 // Remove all grades from gradebook. 1402 $DB->delete_records_select('quiz_grades', 1403 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid)); 1404 if (empty($data->reset_gradebook_grades)) { 1405 quiz_reset_gradebook($data->courseid); 1406 } 1407 $status[] = array( 1408 'component' => $componentstr, 1409 'item' => get_string('gradesdeleted', 'quiz'), 1410 'error' => false); 1411 } 1412 1413 // Remove user overrides. 1414 if (!empty($data->reset_quiz_user_overrides)) { 1415 $DB->delete_records_select('quiz_overrides', 1416 'quiz IN (SELECT id FROM {quiz} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid)); 1417 $status[] = array( 1418 'component' => $componentstr, 1419 'item' => get_string('useroverridesdeleted', 'quiz'), 1420 'error' => false); 1421 } 1422 // Remove group overrides. 1423 if (!empty($data->reset_quiz_group_overrides)) { 1424 $DB->delete_records_select('quiz_overrides', 1425 'quiz IN (SELECT id FROM {quiz} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid)); 1426 $status[] = array( 1427 'component' => $componentstr, 1428 'item' => get_string('groupoverridesdeleted', 'quiz'), 1429 'error' => false); 1430 } 1431 1432 // Updating dates - shift may be negative too. 1433 if ($data->timeshift) { 1434 $DB->execute("UPDATE {quiz_overrides} 1435 SET timeopen = timeopen + ? 1436 WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?) 1437 AND timeopen <> 0", array($data->timeshift, $data->courseid)); 1438 $DB->execute("UPDATE {quiz_overrides} 1439 SET timeclose = timeclose + ? 1440 WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?) 1441 AND timeclose <> 0", array($data->timeshift, $data->courseid)); 1442 1443 shift_course_mod_dates('quiz', array('timeopen', 'timeclose'), 1444 $data->timeshift, $data->courseid); 1445 1446 $status[] = array( 1447 'component' => $componentstr, 1448 'item' => get_string('openclosedatesupdated', 'quiz'), 1449 'error' => false); 1450 } 1451 1452 return $status; 1453 } 1454 1455 /** 1456 * Prints quiz summaries on MyMoodle Page 1457 * @param arry $courses 1458 * @param array $htmlarray 1459 */ 1460 function quiz_print_overview($courses, &$htmlarray) { 1461 global $USER, $CFG; 1462 // These next 6 Lines are constant in all modules (just change module name). 1463 if (empty($courses) || !is_array($courses) || count($courses) == 0) { 1464 return array(); 1465 } 1466 1467 if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) { 1468 return; 1469 } 1470 1471 // Get the quizzes attempts. 1472 $attemptsinfo = []; 1473 $quizids = []; 1474 foreach ($quizzes as $quiz) { 1475 $quizids[] = $quiz->id; 1476 $attemptsinfo[$quiz->id] = ['count' => 0, 'hasfinished' => false]; 1477 } 1478 $attempts = quiz_get_user_attempts($quizids, $USER->id); 1479 foreach ($attempts as $attempt) { 1480 $attemptsinfo[$attempt->quiz]['count']++; 1481 $attemptsinfo[$attempt->quiz]['hasfinished'] = true; 1482 } 1483 unset($attempts); 1484 1485 // Fetch some language strings outside the main loop. 1486 $strquiz = get_string('modulename', 'quiz'); 1487 $strnoattempts = get_string('noattempts', 'quiz'); 1488 1489 // We want to list quizzes that are currently available, and which have a close date. 1490 // This is the same as what the lesson does, and the dabate is in MDL-10568. 1491 $now = time(); 1492 foreach ($quizzes as $quiz) { 1493 if ($quiz->timeclose >= $now && $quiz->timeopen < $now) { 1494 $str = ''; 1495 1496 // Now provide more information depending on the uers's role. 1497 $context = context_module::instance($quiz->coursemodule); 1498 if (has_capability('mod/quiz:viewreports', $context)) { 1499 // For teacher-like people, show a summary of the number of student attempts. 1500 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm 1501 // fields set to make the following call work. 1502 $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>'; 1503 1504 } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student 1505 // For student-like people, tell them how many attempts they have made. 1506 1507 if (isset($USER->id)) { 1508 if ($attemptsinfo[$quiz->id]['hasfinished']) { 1509 // The student's last attempt is finished. 1510 continue; 1511 } 1512 1513 if ($attemptsinfo[$quiz->id]['count'] > 0) { 1514 $str .= '<div class="info">' . 1515 get_string('numattemptsmade', 'quiz', $attemptsinfo[$quiz->id]['count']) . '</div>'; 1516 } else { 1517 $str .= '<div class="info">' . $strnoattempts . '</div>'; 1518 } 1519 1520 } else { 1521 $str .= '<div class="info">' . $strnoattempts . '</div>'; 1522 } 1523 1524 } else { 1525 // For ayone else, there is no point listing this quiz, so stop processing. 1526 continue; 1527 } 1528 1529 // Give a link to the quiz, and the deadline. 1530 $html = '<div class="quiz overview">' . 1531 '<div class="name">' . $strquiz . ': <a ' . 1532 ($quiz->visible ? '' : ' class="dimmed"') . 1533 ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' . 1534 $quiz->coursemodule . '">' . 1535 $quiz->name . '</a></div>'; 1536 $html .= '<div class="info">' . get_string('quizcloseson', 'quiz', 1537 userdate($quiz->timeclose)) . '</div>'; 1538 $html .= $str; 1539 $html .= '</div>'; 1540 if (empty($htmlarray[$quiz->course]['quiz'])) { 1541 $htmlarray[$quiz->course]['quiz'] = $html; 1542 } else { 1543 $htmlarray[$quiz->course]['quiz'] .= $html; 1544 } 1545 } 1546 } 1547 } 1548 1549 /** 1550 * Return a textual summary of the number of attempts that have been made at a particular quiz, 1551 * returns '' if no attempts have been made yet, unless $returnzero is passed as true. 1552 * 1553 * @param object $quiz the quiz object. Only $quiz->id is used at the moment. 1554 * @param object $cm the cm object. Only $cm->course, $cm->groupmode and 1555 * $cm->groupingid fields are used at the moment. 1556 * @param bool $returnzero if false (default), when no attempts have been 1557 * made '' is returned instead of 'Attempts: 0'. 1558 * @param int $currentgroup if there is a concept of current group where this method is being called 1559 * (e.g. a report) pass it in here. Default 0 which means no current group. 1560 * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or 1561 * "Attemtps 123 (45 from this group)". 1562 */ 1563 function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) { 1564 global $DB, $USER; 1565 $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0)); 1566 if ($numattempts || $returnzero) { 1567 if (groups_get_activity_groupmode($cm)) { 1568 $a = new stdClass(); 1569 $a->total = $numattempts; 1570 if ($currentgroup) { 1571 $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' . 1572 '{quiz_attempts} qa JOIN ' . 1573 '{groups_members} gm ON qa.userid = gm.userid ' . 1574 'WHERE quiz = ? AND preview = 0 AND groupid = ?', 1575 array($quiz->id, $currentgroup)); 1576 return get_string('attemptsnumthisgroup', 'quiz', $a); 1577 } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) { 1578 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups)); 1579 $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' . 1580 '{quiz_attempts} qa JOIN ' . 1581 '{groups_members} gm ON qa.userid = gm.userid ' . 1582 'WHERE quiz = ? AND preview = 0 AND ' . 1583 "groupid $usql", array_merge(array($quiz->id), $params)); 1584 return get_string('attemptsnumyourgroups', 'quiz', $a); 1585 } 1586 } 1587 return get_string('attemptsnum', 'quiz', $numattempts); 1588 } 1589 return ''; 1590 } 1591 1592 /** 1593 * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link 1594 * to the quiz reports. 1595 * 1596 * @param object $quiz the quiz object. Only $quiz->id is used at the moment. 1597 * @param object $cm the cm object. Only $cm->course, $cm->groupmode and 1598 * $cm->groupingid fields are used at the moment. 1599 * @param object $context the quiz context. 1600 * @param bool $returnzero if false (default), when no attempts have been made 1601 * '' is returned instead of 'Attempts: 0'. 1602 * @param int $currentgroup if there is a concept of current group where this method is being called 1603 * (e.g. a report) pass it in here. Default 0 which means no current group. 1604 * @return string HTML fragment for the link. 1605 */ 1606 function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false, 1607 $currentgroup = 0) { 1608 global $CFG; 1609 $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup); 1610 if (!$summary) { 1611 return ''; 1612 } 1613 1614 require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); 1615 $url = new moodle_url('/mod/quiz/report.php', array( 1616 'id' => $cm->id, 'mode' => quiz_report_default_report($context))); 1617 return html_writer::link($url, $summary); 1618 } 1619 1620 /** 1621 * @param string $feature FEATURE_xx constant for requested feature 1622 * @return bool True if quiz supports feature 1623 */ 1624 function quiz_supports($feature) { 1625 switch($feature) { 1626 case FEATURE_GROUPS: return true; 1627 case FEATURE_GROUPINGS: return true; 1628 case FEATURE_MOD_INTRO: return true; 1629 case FEATURE_COMPLETION_TRACKS_VIEWS: return true; 1630 case FEATURE_COMPLETION_HAS_RULES: return true; 1631 case FEATURE_GRADE_HAS_GRADE: return true; 1632 case FEATURE_GRADE_OUTCOMES: return true; 1633 case FEATURE_BACKUP_MOODLE2: return true; 1634 case FEATURE_SHOW_DESCRIPTION: return true; 1635 case FEATURE_CONTROLS_GRADE_VISIBILITY: return true; 1636 case FEATURE_USES_QUESTIONS: return true; 1637 1638 default: return null; 1639 } 1640 } 1641 1642 /** 1643 * @return array all other caps used in module 1644 */ 1645 function quiz_get_extra_capabilities() { 1646 global $CFG; 1647 require_once($CFG->libdir . '/questionlib.php'); 1648 $caps = question_get_all_capabilities(); 1649 $caps[] = 'moodle/site:accessallgroups'; 1650 return $caps; 1651 } 1652 1653 /** 1654 * This function extends the settings navigation block for the site. 1655 * 1656 * It is safe to rely on PAGE here as we will only ever be within the module 1657 * context when this is called 1658 * 1659 * @param settings_navigation $settings 1660 * @param navigation_node $quiznode 1661 * @return void 1662 */ 1663 function quiz_extend_settings_navigation($settings, $quiznode) { 1664 global $PAGE, $CFG; 1665 1666 // Require {@link questionlib.php} 1667 // Included here as we only ever want to include this file if we really need to. 1668 require_once($CFG->libdir . '/questionlib.php'); 1669 1670 // We want to add these new nodes after the Edit settings node, and before the 1671 // Locally assigned roles node. Of course, both of those are controlled by capabilities. 1672 $keys = $quiznode->get_children_key_list(); 1673 $beforekey = null; 1674 $i = array_search('modedit', $keys); 1675 if ($i === false and array_key_exists(0, $keys)) { 1676 $beforekey = $keys[0]; 1677 } else if (array_key_exists($i + 1, $keys)) { 1678 $beforekey = $keys[$i + 1]; 1679 } 1680 1681 if (has_capability('mod/quiz:manageoverrides', $PAGE->cm->context)) { 1682 $url = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$PAGE->cm->id)); 1683 $node = navigation_node::create(get_string('groupoverrides', 'quiz'), 1684 new moodle_url($url, array('mode'=>'group')), 1685 navigation_node::TYPE_SETTING, null, 'mod_quiz_groupoverrides'); 1686 $quiznode->add_node($node, $beforekey); 1687 1688 $node = navigation_node::create(get_string('useroverrides', 'quiz'), 1689 new moodle_url($url, array('mode'=>'user')), 1690 navigation_node::TYPE_SETTING, null, 'mod_quiz_useroverrides'); 1691 $quiznode->add_node($node, $beforekey); 1692 } 1693 1694 if (has_capability('mod/quiz:manage', $PAGE->cm->context)) { 1695 $node = navigation_node::create(get_string('editquiz', 'quiz'), 1696 new moodle_url('/mod/quiz/edit.php', array('cmid'=>$PAGE->cm->id)), 1697 navigation_node::TYPE_SETTING, null, 'mod_quiz_edit', 1698 new pix_icon('t/edit', '')); 1699 $quiznode->add_node($node, $beforekey); 1700 } 1701 1702 if (has_capability('mod/quiz:preview', $PAGE->cm->context)) { 1703 $url = new moodle_url('/mod/quiz/startattempt.php', 1704 array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey())); 1705 $node = navigation_node::create(get_string('preview', 'quiz'), $url, 1706 navigation_node::TYPE_SETTING, null, 'mod_quiz_preview', 1707 new pix_icon('i/preview', '')); 1708 $quiznode->add_node($node, $beforekey); 1709 } 1710 1711 if (has_any_capability(array('mod/quiz:viewreports', 'mod/quiz:grade'), $PAGE->cm->context)) { 1712 require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); 1713 $reportlist = quiz_report_list($PAGE->cm->context); 1714 1715 $url = new moodle_url('/mod/quiz/report.php', 1716 array('id' => $PAGE->cm->id, 'mode' => reset($reportlist))); 1717 $reportnode = $quiznode->add_node(navigation_node::create(get_string('results', 'quiz'), $url, 1718 navigation_node::TYPE_SETTING, 1719 null, null, new pix_icon('i/report', '')), $beforekey); 1720 1721 foreach ($reportlist as $report) { 1722 $url = new moodle_url('/mod/quiz/report.php', 1723 array('id' => $PAGE->cm->id, 'mode' => $report)); 1724 $reportnode->add_node(navigation_node::create(get_string($report, 'quiz_'.$report), $url, 1725 navigation_node::TYPE_SETTING, 1726 null, 'quiz_report_' . $report, new pix_icon('i/item', ''))); 1727 } 1728 } 1729 1730 question_extend_settings_navigation($quiznode, $PAGE->cm->context)->trim_if_empty(); 1731 } 1732 1733 /** 1734 * Serves the quiz files. 1735 * 1736 * @package mod_quiz 1737 * @category files 1738 * @param stdClass $course course object 1739 * @param stdClass $cm course module object 1740 * @param stdClass $context context object 1741 * @param string $filearea file area 1742 * @param array $args extra arguments 1743 * @param bool $forcedownload whether or not force download 1744 * @param array $options additional options affecting the file serving 1745 * @return bool false if file not found, does not return if found - justsend the file 1746 */ 1747 function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { 1748 global $CFG, $DB; 1749 1750 if ($context->contextlevel != CONTEXT_MODULE) { 1751 return false; 1752 } 1753 1754 require_login($course, false, $cm); 1755 1756 if (!$quiz = $DB->get_record('quiz', array('id'=>$cm->instance))) { 1757 return false; 1758 } 1759 1760 // The 'intro' area is served by pluginfile.php. 1761 $fileareas = array('feedback'); 1762 if (!in_array($filearea, $fileareas)) { 1763 return false; 1764 } 1765 1766 $feedbackid = (int)array_shift($args); 1767 if (!$feedback = $DB->get_record('quiz_feedback', array('id'=>$feedbackid))) { 1768 return false; 1769 } 1770 1771 $fs = get_file_storage(); 1772 $relativepath = implode('/', $args); 1773 $fullpath = "/$context->id/mod_quiz/$filearea/$feedbackid/$relativepath"; 1774 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { 1775 return false; 1776 } 1777 send_stored_file($file, 0, 0, true, $options); 1778 } 1779 1780 /** 1781 * Called via pluginfile.php -> question_pluginfile to serve files belonging to 1782 * a question in a question_attempt when that attempt is a quiz attempt. 1783 * 1784 * @package mod_quiz 1785 * @category files 1786 * @param stdClass $course course settings object 1787 * @param stdClass $context context object 1788 * @param string $component the name of the component we are serving files for. 1789 * @param string $filearea the name of the file area. 1790 * @param int $qubaid the attempt usage id. 1791 * @param int $slot the id of a question in this quiz attempt. 1792 * @param array $args the remaining bits of the file path. 1793 * @param bool $forcedownload whether the user must be forced to download the file. 1794 * @param array $options additional options affecting the file serving 1795 * @return bool false if file not found, does not return if found - justsend the file 1796 */ 1797 function quiz_question_pluginfile($course, $context, $component, 1798 $filearea, $qubaid, $slot, $args, $forcedownload, array $options=array()) { 1799 global $CFG; 1800 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 1801 1802 $attemptobj = quiz_attempt::create_from_usage_id($qubaid); 1803 require_login($attemptobj->get_course(), false, $attemptobj->get_cm()); 1804 1805 if ($attemptobj->is_own_attempt() && !$attemptobj->is_finished()) { 1806 // In the middle of an attempt. 1807 if (!$attemptobj->is_preview_user()) { 1808 $attemptobj->require_capability('mod/quiz:attempt'); 1809 } 1810 $isreviewing = false; 1811 1812 } else { 1813 // Reviewing an attempt. 1814 $attemptobj->check_review_capability(); 1815 $isreviewing = true; 1816 } 1817 1818 if (!$attemptobj->check_file_access($slot, $isreviewing, $context->id, 1819 $component, $filearea, $args, $forcedownload)) { 1820 send_file_not_found(); 1821 } 1822 1823 $fs = get_file_storage(); 1824 $relativepath = implode('/', $args); 1825 $fullpath = "/$context->id/$component/$filearea/$relativepath"; 1826 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { 1827 send_file_not_found(); 1828 } 1829 1830 send_stored_file($file, 0, 0, $forcedownload, $options); 1831 } 1832 1833 /** 1834 * Return a list of page types 1835 * @param string $pagetype current page type 1836 * @param stdClass $parentcontext Block's parent context 1837 * @param stdClass $currentcontext Current context of block 1838 */ 1839 function quiz_page_type_list($pagetype, $parentcontext, $currentcontext) { 1840 $module_pagetype = array( 1841 'mod-quiz-*' => get_string('page-mod-quiz-x', 'quiz'), 1842 'mod-quiz-view' => get_string('page-mod-quiz-view', 'quiz'), 1843 'mod-quiz-attempt' => get_string('page-mod-quiz-attempt', 'quiz'), 1844 'mod-quiz-summary' => get_string('page-mod-quiz-summary', 'quiz'), 1845 'mod-quiz-review' => get_string('page-mod-quiz-review', 'quiz'), 1846 'mod-quiz-edit' => get_string('page-mod-quiz-edit', 'quiz'), 1847 'mod-quiz-report' => get_string('page-mod-quiz-report', 'quiz'), 1848 ); 1849 return $module_pagetype; 1850 } 1851 1852 /** 1853 * @return the options for quiz navigation. 1854 */ 1855 function quiz_get_navigation_options() { 1856 return array( 1857 QUIZ_NAVMETHOD_FREE => get_string('navmethod_free', 'quiz'), 1858 QUIZ_NAVMETHOD_SEQ => get_string('navmethod_seq', 'quiz') 1859 ); 1860 } 1861 1862 /** 1863 * Obtains the automatic completion state for this quiz on any conditions 1864 * in quiz settings, such as if all attempts are used or a certain grade is achieved. 1865 * 1866 * @param object $course Course 1867 * @param object $cm Course-module 1868 * @param int $userid User ID 1869 * @param bool $type Type of comparison (or/and; can be used as return value if no conditions) 1870 * @return bool True if completed, false if not. (If no conditions, then return 1871 * value depends on comparison type) 1872 */ 1873 function quiz_get_completion_state($course, $cm, $userid, $type) { 1874 global $DB; 1875 global $CFG; 1876 1877 $quiz = $DB->get_record('quiz', array('id' => $cm->instance), '*', MUST_EXIST); 1878 if (!$quiz->completionattemptsexhausted && !$quiz->completionpass) { 1879 return $type; 1880 } 1881 1882 // Check if the user has used up all attempts. 1883 if ($quiz->completionattemptsexhausted) { 1884 $attempts = quiz_get_user_attempts($quiz->id, $userid, 'finished', true); 1885 if ($attempts) { 1886 $lastfinishedattempt = end($attempts); 1887 $context = context_module::instance($cm->id); 1888 $quizobj = quiz::create($quiz->id, $userid); 1889 $accessmanager = new quiz_access_manager($quizobj, time(), 1890 has_capability('mod/quiz:ignoretimelimits', $context, $userid, false)); 1891 if ($accessmanager->is_finished(count($attempts), $lastfinishedattempt)) { 1892 return true; 1893 } 1894 } 1895 } 1896 1897 // Check for passing grade. 1898 if ($quiz->completionpass) { 1899 require_once($CFG->libdir . '/gradelib.php'); 1900 $item = grade_item::fetch(array('courseid' => $course->id, 'itemtype' => 'mod', 1901 'itemmodule' => 'quiz', 'iteminstance' => $cm->instance, 'outcomeid' => null)); 1902 if ($item) { 1903 $grades = grade_grade::fetch_users_grades($item, array($userid), false); 1904 if (!empty($grades[$userid])) { 1905 return $grades[$userid]->is_passed($item); 1906 } 1907 } 1908 } 1909 return false; 1910 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |