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