MDL-29254 Fixed whitespace
[moodle.git] / mod / quiz / lib.php
blob0d5539ff57d8f61d9bc60f0f571c34ae7db8461f
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.
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/>.
17 /**
18 * Library of functions for the quiz module.
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}
23 * @package mod
24 * @subpackage quiz
25 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30 defined('MOODLE_INTERNAL') || die();
32 require_once($CFG->libdir . '/eventslib.php');
33 require_once($CFG->dirroot . '/calendar/lib.php');
36 /**#@+
37 * Option controlling what options are offered on the quiz settings form.
39 define('QUIZ_MAX_ATTEMPT_OPTION', 10);
40 define('QUIZ_MAX_QPP_OPTION', 50);
41 define('QUIZ_MAX_DECIMAL_OPTION', 5);
42 define('QUIZ_MAX_Q_DECIMAL_OPTION', 7);
43 /**#@-*/
45 /**#@+
46 * Options determining how the grades from individual attempts are combined to give
47 * the overall grade for a user
49 define('QUIZ_GRADEHIGHEST', '1');
50 define('QUIZ_GRADEAVERAGE', '2');
51 define('QUIZ_ATTEMPTFIRST', '3');
52 define('QUIZ_ATTEMPTLAST', '4');
53 /**#@-*/
55 /**
56 * If start and end date for the quiz are more than this many seconds apart
57 * they will be represented by two separate events in the calendar
59 define('QUIZ_MAX_EVENT_LENGTH', 5*24*60*60); // 5 days
61 /**
62 * Given an object containing all the necessary data,
63 * (defined by the form in mod_form.php) this function
64 * will create a new instance and return the id number
65 * of the new instance.
67 * @param object $quiz the data that came from the form.
68 * @return mixed the id of the new instance on success,
69 * false or a string error message on failure.
71 function quiz_add_instance($quiz) {
72 global $DB;
73 $cmid = $quiz->coursemodule;
75 // Process the options from the form.
76 $quiz->created = time();
77 $quiz->questions = '';
78 $result = quiz_process_options($quiz);
79 if ($result && is_string($result)) {
80 return $result;
83 // Try to store it in the database.
84 $quiz->id = $DB->insert_record('quiz', $quiz);
86 // Do the processing required after an add or an update.
87 quiz_after_add_or_update($quiz);
89 return $quiz->id;
92 /**
93 * Given an object containing all the necessary data,
94 * (defined by the form in mod_form.php) this function
95 * will update an existing instance with new data.
97 * @param object $quiz the data that came from the form.
98 * @return mixed true on success, false or a string error message on failure.
100 function quiz_update_instance($quiz, $mform) {
101 global $CFG, $DB;
103 // Process the options from the form.
104 $result = quiz_process_options($quiz);
105 if ($result && is_string($result)) {
106 return $result;
109 $oldquiz = $DB->get_record('quiz', array('id' => $quiz->instance));
111 // Repaginate, if asked to.
112 if (!$quiz->shufflequestions && !empty($quiz->repaginatenow)) {
113 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
114 $quiz->questions = quiz_repaginate(quiz_clean_layout($oldquiz->questions, true),
115 $quiz->questionsperpage);
117 unset($quiz->repaginatenow);
119 // Update the database.
120 $quiz->id = $quiz->instance;
121 $DB->update_record('quiz', $quiz);
123 // Do the processing required after an add or an update.
124 quiz_after_add_or_update($quiz);
126 if ($oldquiz->grademethod != $quiz->grademethod) {
127 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
128 $quiz->sumgrades = $oldquiz->sumgrades;
129 $quiz->grade = $oldquiz->grade;
130 quiz_update_all_final_grades($quiz);
131 quiz_update_grades($quiz);
134 // Delete any previous preview attempts
135 quiz_delete_previews($quiz);
137 return true;
141 * Given an ID of an instance of this module,
142 * this function will permanently delete the instance
143 * and any data that depends on it.
145 * @param int $id the id of the quiz to delete.
146 * @return bool success or failure.
148 function quiz_delete_instance($id) {
149 global $DB;
151 $quiz = $DB->get_record('quiz', array('id' => $id), '*', MUST_EXIST);
153 quiz_delete_all_attempts($quiz);
154 quiz_delete_all_overrides($quiz);
156 $DB->delete_records('quiz_question_instances', array('quiz' => $quiz->id));
157 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
159 $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id));
160 foreach ($events as $event) {
161 $event = calendar_event::load($event);
162 $event->delete();
165 quiz_grade_item_delete($quiz);
166 $DB->delete_records('quiz', array('id' => $quiz->id));
168 return true;
172 * Deletes a quiz override from the database and clears any corresponding calendar events
174 * @param object $quiz The quiz object.
175 * @param int $overrideid The id of the override being deleted
176 * @return bool true on success
178 function quiz_delete_override($quiz, $overrideid) {
179 global $DB;
181 $override = $DB->get_record('quiz_overrides', array('id' => $overrideid), '*', MUST_EXIST);
183 // Delete the events
184 $events = $DB->get_records('event', array('modulename' => 'quiz',
185 'instance' => $quiz->id, 'groupid' => (int)$override->groupid,
186 'userid' => (int)$override->userid));
187 foreach ($events as $event) {
188 $eventold = calendar_event::load($event);
189 $eventold->delete();
192 $DB->delete_records('quiz_overrides', array('id' => $overrideid));
193 return true;
197 * Deletes all quiz overrides from the database and clears any corresponding calendar events
199 * @param object $quiz The quiz object.
201 function quiz_delete_all_overrides($quiz) {
202 global $DB;
204 $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id');
205 foreach ($overrides as $override) {
206 quiz_delete_override($quiz, $override->id);
211 * Updates a quiz object with override information for a user.
213 * Algorithm: For each quiz setting, if there is a matching user-specific override,
214 * then use that otherwise, if there are group-specific overrides, return the most
215 * lenient combination of them. If neither applies, leave the quiz setting unchanged.
217 * Special case: if there is more than one password that applies to the user, then
218 * quiz->extrapasswords will contain an array of strings giving the remaining
219 * passwords.
221 * @param object $quiz The quiz object.
222 * @param int $userid The userid.
223 * @return object $quiz The updated quiz object.
225 function quiz_update_effective_access($quiz, $userid) {
226 global $DB;
228 // check for user override
229 $override = $DB->get_record('quiz_overrides', array('quiz' => $quiz->id, 'userid' => $userid));
231 if (!$override) {
232 $override = new stdClass();
233 $override->timeopen = null;
234 $override->timeclose = null;
235 $override->timelimit = null;
236 $override->attempts = null;
237 $override->password = null;
240 // check for group overrides
241 $groupings = groups_get_user_groups($quiz->course, $userid);
243 if (!empty($groupings[0])) {
244 // Select all overrides that apply to the User's groups
245 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
246 $sql = "SELECT * FROM {quiz_overrides}
247 WHERE groupid $extra AND quiz = ?";
248 $params[] = $quiz->id;
249 $records = $DB->get_records_sql($sql, $params);
251 // Combine the overrides
252 $opens = array();
253 $closes = array();
254 $limits = array();
255 $attempts = array();
256 $passwords = array();
258 foreach ($records as $gpoverride) {
259 if (isset($gpoverride->timeopen)) {
260 $opens[] = $gpoverride->timeopen;
262 if (isset($gpoverride->timeclose)) {
263 $closes[] = $gpoverride->timeclose;
265 if (isset($gpoverride->timelimit)) {
266 $limits[] = $gpoverride->timelimit;
268 if (isset($gpoverride->attempts)) {
269 $attempts[] = $gpoverride->attempts;
271 if (isset($gpoverride->password)) {
272 $passwords[] = $gpoverride->password;
275 // If there is a user override for a setting, ignore the group override
276 if (is_null($override->timeopen) && count($opens)) {
277 $override->timeopen = min($opens);
279 if (is_null($override->timeclose) && count($closes)) {
280 $override->timeclose = max($closes);
282 if (is_null($override->timelimit) && count($limits)) {
283 $override->timelimit = max($limits);
285 if (is_null($override->attempts) && count($attempts)) {
286 $override->attempts = max($attempts);
288 if (is_null($override->password) && count($passwords)) {
289 $override->password = array_shift($passwords);
290 if (count($passwords)) {
291 $override->extrapasswords = $passwords;
297 // merge with quiz defaults
298 $keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password', 'extrapasswords');
299 foreach ($keys as $key) {
300 if (isset($override->{$key})) {
301 $quiz->{$key} = $override->{$key};
305 return $quiz;
309 * Delete all the attempts belonging to a quiz.
311 * @param object $quiz The quiz object.
313 function quiz_delete_all_attempts($quiz) {
314 global $CFG, $DB;
315 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
316 question_engine::delete_questions_usage_by_activities(new qubaids_for_quiz($quiz->id));
317 $DB->delete_records('quiz_attempts', array('quiz' => $quiz->id));
318 $DB->delete_records('quiz_grades', array('quiz' => $quiz->id));
322 * Get the best current grade for a particular user in a quiz.
324 * @param object $quiz the quiz settings.
325 * @param int $userid the id of the user.
326 * @return float the user's current grade for this quiz, or null if this user does
327 * not have a grade on this quiz.
329 function quiz_get_best_grade($quiz, $userid) {
330 global $DB;
331 $grade = $DB->get_field('quiz_grades', 'grade',
332 array('quiz' => $quiz->id, 'userid' => $userid));
334 // Need to detect errors/no result, without catching 0 grades.
335 if ($grade === false) {
336 return null;
339 return $grade + 0; // Convert to number.
343 * Is this a graded quiz? If this method returns true, you can assume that
344 * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to
345 * divide by them).
347 * @param object $quiz a row from the quiz table.
348 * @return bool whether this is a graded quiz.
350 function quiz_has_grades($quiz) {
351 return $quiz->grade >= 0.000005 && $quiz->sumgrades >= 0.000005;
355 * Return a small object with summary information about what a
356 * user has done with a given particular instance of this module
357 * Used for user activity reports.
358 * $return->time = the time they did it
359 * $return->info = a short text description
361 * @param object $course
362 * @param object $user
363 * @param object $mod
364 * @param object $quiz
365 * @return object|null
367 function quiz_user_outline($course, $user, $mod, $quiz) {
368 global $DB, $CFG;
369 require_once("$CFG->libdir/gradelib.php");
370 $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
372 if (empty($grades->items[0]->grades)) {
373 return null;
374 } else {
375 $grade = reset($grades->items[0]->grades);
378 $result = new stdClass();
379 $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
381 //datesubmitted == time created. dategraded == time modified or time overridden
382 //if grade was last modified by the user themselves use date graded. Otherwise use
383 // date submitted
384 // TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
385 if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
386 $result->time = $grade->dategraded;
387 } else {
388 $result->time = $grade->datesubmitted;
391 return $result;
395 * Print a detailed representation of what a user has done with
396 * a given particular instance of this module, for user activity reports.
398 * @global object
399 * @param object $course
400 * @param object $user
401 * @param object $mod
402 * @param object $quiz
403 * @return bool
405 function quiz_user_complete($course, $user, $mod, $quiz) {
406 global $DB, $CFG, $OUTPUT;
407 require_once("$CFG->libdir/gradelib.php");
409 $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
410 if (!empty($grades->items[0]->grades)) {
411 $grade = reset($grades->items[0]->grades);
412 echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
413 if ($grade->str_feedback) {
414 echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
418 if ($attempts = $DB->get_records('quiz_attempts',
419 array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) {
420 foreach ($attempts as $attempt) {
421 echo get_string('attempt', 'quiz').' '.$attempt->attempt.': ';
422 if ($attempt->timefinish == 0) {
423 print_string('unfinished');
424 } else {
425 echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' .
426 quiz_format_grade($quiz, $quiz->sumgrades);
428 echo ' - '.userdate($attempt->timemodified).'<br />';
430 } else {
431 print_string('noattempts', 'quiz');
434 return true;
438 * Quiz periodic clean-up tasks.
440 function quiz_cron() {
442 // Run cron for our sub-plugin types.
443 cron_execute_plugin_type('quiz', 'quiz reports');
444 cron_execute_plugin_type('quizaccess', 'quiz access rules');
448 * @param int $quizid the quiz id.
449 * @param int $userid the userid.
450 * @param string $status 'all', 'finished' or 'unfinished' to control
451 * @param bool $includepreviews
452 * @return an array of all the user's attempts at this quiz. Returns an empty
453 * array if there are none.
455 function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
456 global $DB;
457 $status_condition = array(
458 'all' => '',
459 'finished' => ' AND timefinish > 0',
460 'unfinished' => ' AND timefinish = 0'
462 $previewclause = '';
463 if (!$includepreviews) {
464 $previewclause = ' AND preview = 0';
466 return $DB->get_records_select('quiz_attempts',
467 'quiz = ? AND userid = ?' . $previewclause . $status_condition[$status],
468 array($quizid, $userid), 'attempt ASC');
472 * Return grade for given user or all users.
474 * @param int $quizid id of quiz
475 * @param int $userid optional user id, 0 means all users
476 * @return array array of grades, false if none. These are raw grades. They should
477 * be processed with quiz_format_grade for display.
479 function quiz_get_user_grades($quiz, $userid = 0) {
480 global $CFG, $DB;
482 $params = array($quiz->id);
483 $usertest = '';
484 if ($userid) {
485 $params[] = $userid;
486 $usertest = 'AND u.id = ?';
488 return $DB->get_records_sql("
489 SELECT
490 u.id,
491 u.id AS userid,
492 qg.grade AS rawgrade,
493 qg.timemodified AS dategraded,
494 MAX(qa.timefinish) AS datesubmitted
496 FROM {user} u
497 JOIN {quiz_grades} qg ON u.id = qg.userid
498 JOIN {quiz_attempts} qa ON qa.quiz = qg.quiz AND qa.userid = u.id
500 WHERE qg.quiz = ?
501 $usertest
502 GROUP BY u.id, qg.grade, qg.timemodified", $params);
506 * Round a grade to to the correct number of decimal places, and format it for display.
508 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
509 * @param float $grade The grade to round.
510 * @return float
512 function quiz_format_grade($quiz, $grade) {
513 if (is_null($grade)) {
514 return get_string('notyetgraded', 'quiz');
516 return format_float($grade, $quiz->decimalpoints);
520 * Round a grade to to the correct number of decimal places, and format it for display.
522 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
523 * @param float $grade The grade to round.
524 * @return float
526 function quiz_format_question_grade($quiz, $grade) {
527 if (empty($quiz->questiondecimalpoints)) {
528 $quiz->questiondecimalpoints = -1;
530 if ($quiz->questiondecimalpoints == -1) {
531 return format_float($grade, $quiz->decimalpoints);
532 } else {
533 return format_float($grade, $quiz->questiondecimalpoints);
538 * Update grades in central gradebook
540 * @param object $quiz the quiz settings.
541 * @param int $userid specific user only, 0 means all users.
543 function quiz_update_grades($quiz, $userid = 0, $nullifnone = true) {
544 global $CFG, $DB;
545 require_once($CFG->libdir.'/gradelib.php');
547 if ($quiz->grade == 0) {
548 quiz_grade_item_update($quiz);
550 } else if ($grades = quiz_get_user_grades($quiz, $userid)) {
551 quiz_grade_item_update($quiz, $grades);
553 } else if ($userid && $nullifnone) {
554 $grade = new stdClass();
555 $grade->userid = $userid;
556 $grade->rawgrade = null;
557 quiz_grade_item_update($quiz, $grade);
559 } else {
560 quiz_grade_item_update($quiz);
565 * Update all grades in gradebook.
567 function quiz_upgrade_grades() {
568 global $DB;
570 $sql = "SELECT COUNT('x')
571 FROM {quiz} a, {course_modules} cm, {modules} m
572 WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
573 $count = $DB->count_records_sql($sql);
575 $sql = "SELECT a.*, cm.idnumber AS cmidnumber, a.course AS courseid
576 FROM {quiz} a, {course_modules} cm, {modules} m
577 WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
578 $rs = $DB->get_recordset_sql($sql);
579 if ($rs->valid()) {
580 $pbar = new progress_bar('quizupgradegrades', 500, true);
581 $i=0;
582 foreach ($rs as $quiz) {
583 $i++;
584 upgrade_set_timeout(60*5); // set up timeout, may also abort execution
585 quiz_update_grades($quiz, 0, false);
586 $pbar->update($i, $count, "Updating Quiz grades ($i/$count).");
589 $rs->close();
593 * Create grade item for given quiz
595 * @param object $quiz object with extra cmidnumber
596 * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
597 * @return int 0 if ok, error code otherwise
599 function quiz_grade_item_update($quiz, $grades = null) {
600 global $CFG, $OUTPUT;
601 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
602 require_once($CFG->libdir.'/gradelib.php');
604 if (array_key_exists('cmidnumber', $quiz)) { // may not be always present
605 $params = array('itemname' => $quiz->name, 'idnumber' => $quiz->cmidnumber);
606 } else {
607 $params = array('itemname' => $quiz->name);
610 if ($quiz->grade > 0) {
611 $params['gradetype'] = GRADE_TYPE_VALUE;
612 $params['grademax'] = $quiz->grade;
613 $params['grademin'] = 0;
615 } else {
616 $params['gradetype'] = GRADE_TYPE_NONE;
619 // description by TJ:
620 // 1. If the quiz is set to not show grades while the quiz is still open,
621 // and is set to show grades after the quiz is closed, then create the
622 // grade_item with a show-after date that is the quiz close date.
623 // 2. If the quiz is set to not show grades at either of those times,
624 // create the grade_item as hidden.
625 // 3. If the quiz is set to show grades, create the grade_item visible.
626 $openreviewoptions = mod_quiz_display_options::make_from_quiz($quiz,
627 mod_quiz_display_options::LATER_WHILE_OPEN);
628 $closedreviewoptions = mod_quiz_display_options::make_from_quiz($quiz,
629 mod_quiz_display_options::AFTER_CLOSE);
630 if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX &&
631 $closedreviewoptions->marks < question_display_options::MARK_AND_MAX) {
632 $params['hidden'] = 1;
634 } else if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX &&
635 $closedreviewoptions->marks >= question_display_options::MARK_AND_MAX) {
636 if ($quiz->timeclose) {
637 $params['hidden'] = $quiz->timeclose;
638 } else {
639 $params['hidden'] = 1;
642 } else {
643 // a) both open and closed enabled
644 // b) open enabled, closed disabled - we can not "hide after",
645 // grades are kept visible even after closing
646 $params['hidden'] = 0;
649 if ($grades === 'reset') {
650 $params['reset'] = true;
651 $grades = null;
654 $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
655 if (!empty($gradebook_grades->items)) {
656 $grade_item = $gradebook_grades->items[0];
657 if ($grade_item->locked) {
658 $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
659 if (!$confirm_regrade) {
660 $message = get_string('gradeitemislocked', 'grades');
661 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id .
662 '&amp;mode=overview';
663 $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
664 echo $OUTPUT->box_start('generalbox', 'notice');
665 echo '<p>'. $message .'</p>';
666 echo $OUTPUT->container_start('buttons');
667 echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades'));
668 echo $OUTPUT->single_button($back_link, get_string('cancel'));
669 echo $OUTPUT->container_end();
670 echo $OUTPUT->box_end();
672 return GRADE_UPDATE_ITEM_LOCKED;
677 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
681 * Delete grade item for given quiz
683 * @param object $quiz object
684 * @return object quiz
686 function quiz_grade_item_delete($quiz) {
687 global $CFG;
688 require_once($CFG->libdir . '/gradelib.php');
690 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0,
691 null, array('deleted' => 1));
695 * Returns an array of users who have data in a given quiz
697 * @todo: deprecated - to be deleted in 2.2
699 * @param int $quizid the quiz id.
700 * @return array of userids.
702 function quiz_get_participants($quizid) {
703 global $CFG, $DB;
705 return $DB->get_records_sql('
706 SELECT DISTINCT userid, userid
707 JOIN {quiz_attempts} qa
708 WHERE a.quiz = ?', array($quizid));
712 * This standard function will check all instances of this module
713 * and make sure there are up-to-date events created for each of them.
714 * If courseid = 0, then every quiz event in the site is checked, else
715 * only quiz events belonging to the course specified are checked.
716 * This function is used, in its new format, by restore_refresh_events()
718 * @param int $courseid
719 * @return bool
721 function quiz_refresh_events($courseid = 0) {
722 global $DB;
724 if ($courseid == 0) {
725 if (!$quizzes = $DB->get_records('quiz')) {
726 return true;
728 } else {
729 if (!$quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
730 return true;
734 foreach ($quizzes as $quiz) {
735 quiz_update_events($quiz);
738 return true;
742 * Returns all quiz graded users since a given time for specified quiz
744 function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
745 $courseid, $cmid, $userid = 0, $groupid = 0) {
746 global $CFG, $COURSE, $USER, $DB;
747 require_once('locallib.php');
749 if ($COURSE->id == $courseid) {
750 $course = $COURSE;
751 } else {
752 $course = $DB->get_record('course', array('id' => $courseid));
755 $modinfo =& get_fast_modinfo($course);
757 $cm = $modinfo->cms[$cmid];
758 $quiz = $DB->get_record('quiz', array('id' => $cm->instance));
760 if ($userid) {
761 $userselect = "AND u.id = :userid";
762 $params['userid'] = $userid;
763 } else {
764 $userselect = '';
767 if ($groupid) {
768 $groupselect = 'AND gm.groupid = :groupid';
769 $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id';
770 $params['groupid'] = $groupid;
771 } else {
772 $groupselect = '';
773 $groupjoin = '';
776 $params['timestart'] = $timestart;
777 $params['quizid'] = $quiz->id;
779 if (!$attempts = $DB->get_records_sql("
780 SELECT qa.*,
781 u.firstname, u.lastname, u.email, u.picture, u.imagealt
782 FROM {quiz_attempts} qa
783 JOIN {user} u ON u.id = qa.userid
784 $groupjoin
785 WHERE qa.timefinish > :timestart
786 AND qa.quiz = :quizid
787 AND qa.preview = 0
788 $userselect
789 $groupselect
790 ORDER BY qa.timefinish ASC", $params)) {
791 return;
794 $context = get_context_instance(CONTEXT_MODULE, $cm->id);
795 $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
796 $viewfullnames = has_capability('moodle/site:viewfullnames', $context);
797 $grader = has_capability('mod/quiz:viewreports', $context);
798 $groupmode = groups_get_activity_groupmode($cm, $course);
800 if (is_null($modinfo->groups)) {
801 // load all my groups and cache it in modinfo
802 $modinfo->groups = groups_get_user_groups($course->id);
805 $usersgroups = null;
806 $aname = format_string($cm->name, true);
807 foreach ($attempts as $attempt) {
808 if ($attempt->userid != $USER->id) {
809 if (!$grader) {
810 // Grade permission required
811 continue;
814 if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
815 if (is_null($usersgroups)) {
816 $usersgroups = groups_get_all_groups($course->id,
817 $attempt->userid, $cm->groupingid);
818 if (is_array($usersgroups)) {
819 $usersgroups = array_keys($usersgroups);
820 } else {
821 $usersgroups = array();
824 if (!array_intersect($usersgroups, $modinfo->groups[$cm->id])) {
825 continue;
830 $options = quiz_get_review_options($quiz, $attempt, $context);
832 $tmpactivity = new stdClass();
834 $tmpactivity->type = 'quiz';
835 $tmpactivity->cmid = $cm->id;
836 $tmpactivity->name = $aname;
837 $tmpactivity->sectionnum = $cm->sectionnum;
838 $tmpactivity->timestamp = $attempt->timefinish;
840 $tmpactivity->content->attemptid = $attempt->id;
841 $tmpactivity->content->attempt = $attempt->attempt;
842 if (quiz_has_grades($quiz) && $options->marks >= question_display_options::MARK_AND_MAX) {
843 $tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades);
844 $tmpactivity->content->maxgrade = quiz_format_grade($quiz, $quiz->sumgrades);
845 } else {
846 $tmpactivity->content->sumgrades = null;
847 $tmpactivity->content->maxgrade = null;
850 $tmpactivity->user->id = $attempt->userid;
851 $tmpactivity->user->firstname = $attempt->firstname;
852 $tmpactivity->user->lastname = $attempt->lastname;
853 $tmpactivity->user->fullname = fullname($attempt, $viewfullnames);
854 $tmpactivity->user->picture = $attempt->picture;
855 $tmpactivity->user->imagealt = $attempt->imagealt;
856 $tmpactivity->user->email = $attempt->email;
858 $activities[$index++] = $tmpactivity;
862 function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
863 global $CFG, $OUTPUT;
865 echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">';
867 echo '<tr><td class="userpicture" valign="top">';
868 echo $OUTPUT->user_picture($activity->user, array('courseid' => $courseid));
869 echo '</td><td>';
871 if ($detail) {
872 $modname = $modnames[$activity->type];
873 echo '<div class="title">';
874 echo '<img src="' . $OUTPUT->pix_url('icon', $activity->type) . '" ' .
875 'class="icon" alt="' . $modname . '" />';
876 echo '<a href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
877 $activity->cmid . '">' . $activity->name . '</a>';
878 echo '</div>';
881 echo '<div class="grade">';
882 echo get_string('attempt', 'quiz', $activity->content->attempt);
883 if (isset($activity->content->maxgrade)) {
884 $grades = $activity->content->sumgrades . ' / ' . $activity->content->maxgrade;
885 echo ': (<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?attempt=' .
886 $activity->content->attemptid . '">' . $grades . '</a>)';
888 echo '</div>';
890 echo '<div class="user">';
891 echo '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $activity->user->id .
892 '&amp;course=' . $courseid . '">' . $activity->user->fullname .
893 '</a> - ' . userdate($activity->timestamp);
894 echo '</div>';
896 echo '</td></tr></table>';
898 return;
902 * Pre-process the quiz options form data, making any necessary adjustments.
903 * Called by add/update instance in this file.
905 * @param object $quiz The variables set on the form.
907 function quiz_process_options($quiz) {
908 global $CFG;
909 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
910 require_once($CFG->libdir . '/questionlib.php');
912 $quiz->timemodified = time();
914 // Quiz name.
915 if (!empty($quiz->name)) {
916 $quiz->name = trim($quiz->name);
919 // Password field - different in form to stop browsers that remember passwords
920 // getting confused.
921 $quiz->password = $quiz->quizpassword;
922 unset($quiz->quizpassword);
924 // Quiz feedback
925 if (isset($quiz->feedbacktext)) {
926 // Clean up the boundary text.
927 for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
928 if (empty($quiz->feedbacktext[$i]['text'])) {
929 $quiz->feedbacktext[$i]['text'] = '';
930 } else {
931 $quiz->feedbacktext[$i]['text'] = trim($quiz->feedbacktext[$i]['text']);
935 // Check the boundary value is a number or a percentage, and in range.
936 $i = 0;
937 while (!empty($quiz->feedbackboundaries[$i])) {
938 $boundary = trim($quiz->feedbackboundaries[$i]);
939 if (!is_numeric($boundary)) {
940 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
941 $boundary = trim(substr($boundary, 0, -1));
942 if (is_numeric($boundary)) {
943 $boundary = $boundary * $quiz->grade / 100.0;
944 } else {
945 return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
949 if ($boundary <= 0 || $boundary >= $quiz->grade) {
950 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
952 if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
953 return get_string('feedbackerrororder', 'quiz', $i + 1);
955 $quiz->feedbackboundaries[$i] = $boundary;
956 $i += 1;
958 $numboundaries = $i;
960 // Check there is nothing in the remaining unused fields.
961 if (!empty($quiz->feedbackboundaries)) {
962 for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
963 if (!empty($quiz->feedbackboundaries[$i]) &&
964 trim($quiz->feedbackboundaries[$i]) != '') {
965 return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
969 for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
970 if (!empty($quiz->feedbacktext[$i]['text']) &&
971 trim($quiz->feedbacktext[$i]['text']) != '') {
972 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
975 // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
976 $quiz->feedbackboundaries[-1] = $quiz->grade + 1;
977 $quiz->feedbackboundaries[$numboundaries] = 0;
978 $quiz->feedbackboundarycount = $numboundaries;
981 // Combing the individual settings into the review columns.
982 $quiz->reviewattempt = quiz_review_option_form_to_db($quiz, 'attempt');
983 $quiz->reviewcorrectness = quiz_review_option_form_to_db($quiz, 'correctness');
984 $quiz->reviewmarks = quiz_review_option_form_to_db($quiz, 'marks');
985 $quiz->reviewspecificfeedback = quiz_review_option_form_to_db($quiz, 'specificfeedback');
986 $quiz->reviewgeneralfeedback = quiz_review_option_form_to_db($quiz, 'generalfeedback');
987 $quiz->reviewrightanswer = quiz_review_option_form_to_db($quiz, 'rightanswer');
988 $quiz->reviewoverallfeedback = quiz_review_option_form_to_db($quiz, 'overallfeedback');
989 $quiz->reviewattempt |= mod_quiz_display_options::DURING;
990 $quiz->reviewoverallfeedback &= ~mod_quiz_display_options::DURING;
994 * Helper function for {@link quiz_process_options()}.
995 * @param object $fromform the sumbitted form date.
996 * @param string $field one of the review option field names.
998 function quiz_review_option_form_to_db($fromform, $field) {
999 static $times = array(
1000 'during' => mod_quiz_display_options::DURING,
1001 'immediately' => mod_quiz_display_options::IMMEDIATELY_AFTER,
1002 'open' => mod_quiz_display_options::LATER_WHILE_OPEN,
1003 'closed' => mod_quiz_display_options::AFTER_CLOSE,
1006 $review = 0;
1007 foreach ($times as $whenname => $when) {
1008 $fieldname = $field . $whenname;
1009 if (isset($fromform->$fieldname)) {
1010 $review |= $when;
1011 unset($fromform->$fieldname);
1015 return $review;
1019 * This function is called at the end of quiz_add_instance
1020 * and quiz_update_instance, to do the common processing.
1022 * @param object $quiz the quiz object.
1024 function quiz_after_add_or_update($quiz) {
1025 global $DB;
1026 $cmid = $quiz->coursemodule;
1028 // we need to use context now, so we need to make sure all needed info is already in db
1029 $DB->set_field('course_modules', 'instance', $quiz->id, array('id'=>$cmid));
1030 $context = get_context_instance(CONTEXT_MODULE, $cmid);
1032 // Save the feedback
1033 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
1035 for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) {
1036 $feedback = new stdClass();
1037 $feedback->quizid = $quiz->id;
1038 $feedback->feedbacktext = $quiz->feedbacktext[$i]['text'];
1039 $feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format'];
1040 $feedback->mingrade = $quiz->feedbackboundaries[$i];
1041 $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
1042 $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1043 $feedbacktext = file_save_draft_area_files((int)$quiz->feedbacktext[$i]['itemid'],
1044 $context->id, 'mod_quiz', 'feedback', $feedback->id,
1045 array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0),
1046 $quiz->feedbacktext[$i]['text']);
1047 $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext,
1048 array('id' => $feedback->id));
1051 // Store any settings belonging to the access rules.
1052 quiz_access_manager::save_settings($quiz);
1054 // Update the events relating to this quiz.
1055 quiz_update_events($quiz);
1057 //update related grade item
1058 quiz_grade_item_update($quiz);
1062 * This function updates the events associated to the quiz.
1063 * If $override is non-zero, then it updates only the events
1064 * associated with the specified override.
1066 * @uses QUIZ_MAX_EVENT_LENGTH
1067 * @param object $quiz the quiz object.
1068 * @param object optional $override limit to a specific override
1070 function quiz_update_events($quiz, $override = null) {
1071 global $DB;
1073 // Load the old events relating to this quiz.
1074 $conds = array('modulename'=>'quiz',
1075 'instance'=>$quiz->id);
1076 if (!empty($override)) {
1077 // only load events for this override
1078 $conds['groupid'] = isset($override->groupid)? $override->groupid : 0;
1079 $conds['userid'] = isset($override->userid)? $override->userid : 0;
1081 $oldevents = $DB->get_records('event', $conds);
1083 // Now make a todo list of all that needs to be updated
1084 if (empty($override)) {
1085 // We are updating the primary settings for the quiz, so we
1086 // need to add all the overrides
1087 $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id));
1088 // as well as the original quiz (empty override)
1089 $overrides[] = new stdClass();
1090 } else {
1091 // Just do the one override
1092 $overrides = array($override);
1095 foreach ($overrides as $current) {
1096 $groupid = isset($current->groupid)? $current->groupid : 0;
1097 $userid = isset($current->userid)? $current->userid : 0;
1098 $timeopen = isset($current->timeopen)? $current->timeopen : $quiz->timeopen;
1099 $timeclose = isset($current->timeclose)? $current->timeclose : $quiz->timeclose;
1101 // only add open/close events for an override if they differ from the quiz default
1102 $addopen = empty($current->id) || !empty($current->timeopen);
1103 $addclose = empty($current->id) || !empty($current->timeclose);
1105 $event = new stdClass();
1106 $event->description = $quiz->intro;
1107 // Events module won't show user events when the courseid is nonzero
1108 $event->courseid = ($userid) ? 0 : $quiz->course;
1109 $event->groupid = $groupid;
1110 $event->userid = $userid;
1111 $event->modulename = 'quiz';
1112 $event->instance = $quiz->id;
1113 $event->timestart = $timeopen;
1114 $event->timeduration = max($timeclose - $timeopen, 0);
1115 $event->visible = instance_is_visible('quiz', $quiz);
1116 $event->eventtype = 'open';
1118 // Determine the event name
1119 if ($groupid) {
1120 $params = new stdClass();
1121 $params->quiz = $quiz->name;
1122 $params->group = groups_get_group_name($groupid);
1123 if ($params->group === false) {
1124 // group doesn't exist, just skip it
1125 continue;
1127 $eventname = get_string('overridegroupeventname', 'quiz', $params);
1128 } else if ($userid) {
1129 $params = new stdClass();
1130 $params->quiz = $quiz->name;
1131 $eventname = get_string('overrideusereventname', 'quiz', $params);
1132 } else {
1133 $eventname = $quiz->name;
1135 if ($addopen or $addclose) {
1136 if ($timeclose and $timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) {
1137 // Single event for the whole quiz.
1138 if ($oldevent = array_shift($oldevents)) {
1139 $event->id = $oldevent->id;
1140 } else {
1141 unset($event->id);
1143 $event->name = $eventname;
1144 // calendar_event::create will reuse a db record if the id field is set
1145 calendar_event::create($event);
1146 } else {
1147 // Separate start and end events.
1148 $event->timeduration = 0;
1149 if ($timeopen && $addopen) {
1150 if ($oldevent = array_shift($oldevents)) {
1151 $event->id = $oldevent->id;
1152 } else {
1153 unset($event->id);
1155 $event->name = $eventname.' ('.get_string('quizopens', 'quiz').')';
1156 // calendar_event::create will reuse a db record if the id field is set
1157 calendar_event::create($event);
1159 if ($timeclose && $addclose) {
1160 if ($oldevent = array_shift($oldevents)) {
1161 $event->id = $oldevent->id;
1162 } else {
1163 unset($event->id);
1165 $event->name = $eventname.' ('.get_string('quizcloses', 'quiz').')';
1166 $event->timestart = $timeclose;
1167 $event->eventtype = 'close';
1168 calendar_event::create($event);
1174 // Delete any leftover events
1175 foreach ($oldevents as $badevent) {
1176 $badevent = calendar_event::load($badevent);
1177 $badevent->delete();
1182 * @return array
1184 function quiz_get_view_actions() {
1185 return array('view', 'view all', 'report', 'review');
1189 * @return array
1191 function quiz_get_post_actions() {
1192 return array('attempt', 'close attempt', 'preview', 'editquestions',
1193 'delete attempt', 'manualgrade');
1197 * @param array $questionids of question ids.
1198 * @return bool whether any of these questions are used by any instance of this module.
1200 function quiz_questions_in_use($questionids) {
1201 global $DB, $CFG;
1202 require_once($CFG->libdir . '/questionlib.php');
1203 list($test, $params) = $DB->get_in_or_equal($questionids);
1204 return $DB->record_exists_select('quiz_question_instances',
1205 'question ' . $test, $params) || question_engine::questions_in_use(
1206 $questionids, new qubaid_join('{quiz_attempts} quiza',
1207 'quiza.uniqueid', 'quiza.preview = 0'));
1211 * Implementation of the function for printing the form elements that control
1212 * whether the course reset functionality affects the quiz.
1214 * @param $mform the course reset form that is being built.
1216 function quiz_reset_course_form_definition($mform) {
1217 $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz'));
1218 $mform->addElement('advcheckbox', 'reset_quiz_attempts',
1219 get_string('removeallquizattempts', 'quiz'));
1223 * Course reset form defaults.
1224 * @return array the defaults.
1226 function quiz_reset_course_form_defaults($course) {
1227 return array('reset_quiz_attempts' => 1);
1231 * Removes all grades from gradebook
1233 * @param int $courseid
1234 * @param string optional type
1236 function quiz_reset_gradebook($courseid, $type='') {
1237 global $CFG, $DB;
1239 $quizzes = $DB->get_records_sql("
1240 SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
1241 FROM {modules} m
1242 JOIN {course_modules} cm ON m.id = cm.module
1243 JOIN {quiz} q ON cm.instance = q.id
1244 WHERE m.name = 'quiz' AND cm.course = ?", array($courseid));
1246 foreach ($quizzes as $quiz) {
1247 quiz_grade_item_update($quiz, 'reset');
1252 * Actual implementation of the reset course functionality, delete all the
1253 * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is
1254 * set and true.
1256 * Also, move the quiz open and close dates, if the course start date is changing.
1258 * @param object $data the data submitted from the reset course.
1259 * @return array status array
1261 function quiz_reset_userdata($data) {
1262 global $CFG, $DB;
1263 require_once($CFG->libdir.'/questionlib.php');
1265 $componentstr = get_string('modulenameplural', 'quiz');
1266 $status = array();
1268 // Delete attempts.
1269 if (!empty($data->reset_quiz_attempts)) {
1270 require_once($CFG->libdir . '/questionlib.php');
1272 question_engine::delete_questions_usage_by_activities(new qubaid_join(
1273 '{quiz_attempts} quiza JOIN {quiz} quiz ON quiza.quiz = quiz.id',
1274 'quiza.uniqueid', 'quiz.course = :quizcourseid',
1275 array('quizcourseid' => $data->courseid)));
1277 $DB->delete_records_select('quiz_attempts',
1278 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid));
1279 $status[] = array(
1280 'component' => $componentstr,
1281 'item' => get_string('attemptsdeleted', 'quiz'),
1282 'error' => false);
1284 // Remove all grades from gradebook
1285 $DB->delete_records_select('quiz_grades',
1286 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid));
1287 if (empty($data->reset_gradebook_grades)) {
1288 quiz_reset_gradebook($data->courseid);
1290 $status[] = array(
1291 'component' => $componentstr,
1292 'item' => get_string('gradesdeleted', 'quiz'),
1293 'error' => false);
1296 // Updating dates - shift may be negative too
1297 if ($data->timeshift) {
1298 shift_course_mod_dates('quiz', array('timeopen', 'timeclose'),
1299 $data->timeshift, $data->courseid);
1300 $status[] = array(
1301 'component' => $componentstr,
1302 'item' => get_string('openclosedatesupdated', 'quiz'),
1303 'error' => false);
1306 return $status;
1310 * Checks whether the current user is allowed to view a file uploaded in a quiz.
1311 * Teachers can view any from their courses, students can only view their own.
1313 * @param int $attemptuniqueid int attempt id
1314 * @param int $questionid int question id
1315 * @return bool to indicate access granted or denied
1317 function quiz_check_file_access($attemptuniqueid, $questionid, $context = null) {
1318 global $USER, $DB, $CFG;
1319 require_once(dirname(__FILE__).'/attemptlib.php');
1320 require_once(dirname(__FILE__).'/locallib.php');
1322 $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $attemptuniqueid));
1323 $attemptobj = quiz_attempt::create($attempt->id);
1325 // does question exist?
1326 if (!$question = $DB->get_record('question', array('id' => $questionid))) {
1327 return false;
1330 if ($context === null) {
1331 $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz));
1332 $cm = get_coursemodule_from_id('quiz', $quiz->id);
1333 $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1336 // Load those questions and the associated states.
1337 $attemptobj->load_questions(array($questionid));
1338 $attemptobj->load_question_states(array($questionid));
1340 // obtain state
1341 $state = $attemptobj->get_question_state($questionid);
1342 // obtain questoin
1343 $question = $attemptobj->get_question($questionid);
1345 // access granted if the current user submitted this file
1346 if ($attempt->userid != $USER->id) {
1347 return false;
1349 // access granted if the current user has permission to grade quizzes in this course
1350 if (!(has_capability('mod/quiz:viewreports', $context) ||
1351 has_capability('mod/quiz:grade', $context))) {
1352 return false;
1355 return array($question, $state, array());
1359 * Prints quiz summaries on MyMoodle Page
1360 * @param arry $courses
1361 * @param array $htmlarray
1363 function quiz_print_overview($courses, &$htmlarray) {
1364 global $USER, $CFG;
1365 // These next 6 Lines are constant in all modules (just change module name)
1366 if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1367 return array();
1370 if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
1371 return;
1374 // Fetch some language strings outside the main loop.
1375 $strquiz = get_string('modulename', 'quiz');
1376 $strnoattempts = get_string('noattempts', 'quiz');
1378 // We want to list quizzes that are currently available, and which have a close date.
1379 // This is the same as what the lesson does, and the dabate is in MDL-10568.
1380 $now = time();
1381 foreach ($quizzes as $quiz) {
1382 if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
1383 // Give a link to the quiz, and the deadline.
1384 $str = '<div class="quiz overview">' .
1385 '<div class="name">' . $strquiz . ': <a ' .
1386 ($quiz->visible ? '' : ' class="dimmed"') .
1387 ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
1388 $quiz->coursemodule . '">' .
1389 $quiz->name . '</a></div>';
1390 $str .= '<div class="info">' . get_string('quizcloseson', 'quiz',
1391 userdate($quiz->timeclose)) . '</div>';
1393 // Now provide more information depending on the uers's role.
1394 $context = get_context_instance(CONTEXT_MODULE, $quiz->coursemodule);
1395 if (has_capability('mod/quiz:viewreports', $context)) {
1396 // For teacher-like people, show a summary of the number of student attempts.
1397 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
1398 // fields set to make the following call work.
1399 $str .= '<div class="info">' .
1400 quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
1401 } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
1402 $context)) { // Student
1403 // For student-like people, tell them how many attempts they have made.
1404 if (isset($USER->id) &&
1405 ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
1406 $numattempts = count($attempts);
1407 $str .= '<div class="info">' .
1408 get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
1409 } else {
1410 $str .= '<div class="info">' . $strnoattempts . '</div>';
1412 } else {
1413 // For ayone else, there is no point listing this quiz, so stop processing.
1414 continue;
1417 // Add the output for this quiz to the rest.
1418 $str .= '</div>';
1419 if (empty($htmlarray[$quiz->course]['quiz'])) {
1420 $htmlarray[$quiz->course]['quiz'] = $str;
1421 } else {
1422 $htmlarray[$quiz->course]['quiz'] .= $str;
1429 * Return a textual summary of the number of attempts that have been made at a particular quiz,
1430 * returns '' if no attempts have been made yet, unless $returnzero is passed as true.
1432 * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1433 * @param object $cm the cm object. Only $cm->course, $cm->groupmode and
1434 * $cm->groupingid fields are used at the moment.
1435 * @param bool $returnzero if false (default), when no attempts have been
1436 * made '' is returned instead of 'Attempts: 0'.
1437 * @param int $currentgroup if there is a concept of current group where this method is being called
1438 * (e.g. a report) pass it in here. Default 0 which means no current group.
1439 * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
1440 * "Attemtps 123 (45 from this group)".
1442 function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
1443 global $DB, $USER;
1444 $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0));
1445 if ($numattempts || $returnzero) {
1446 if (groups_get_activity_groupmode($cm)) {
1447 $a = new stdClass();
1448 $a->total = $numattempts;
1449 if ($currentgroup) {
1450 $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' .
1451 '{quiz_attempts} qa JOIN ' .
1452 '{groups_members} gm ON qa.userid = gm.userid ' .
1453 'WHERE quiz = ? AND preview = 0 AND groupid = ?',
1454 array($quiz->id, $currentgroup));
1455 return get_string('attemptsnumthisgroup', 'quiz', $a);
1456 } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
1457 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups));
1458 $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' .
1459 '{quiz_attempts} qa JOIN ' .
1460 '{groups_members} gm ON qa.userid = gm.userid ' .
1461 'WHERE quiz = ? AND preview = 0 AND ' .
1462 "groupid $usql", array_merge(array($quiz->id), $params));
1463 return get_string('attemptsnumyourgroups', 'quiz', $a);
1466 return get_string('attemptsnum', 'quiz', $numattempts);
1468 return '';
1472 * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
1473 * to the quiz reports.
1475 * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1476 * @param object $cm the cm object. Only $cm->course, $cm->groupmode and
1477 * $cm->groupingid fields are used at the moment.
1478 * @param object $context the quiz context.
1479 * @param bool $returnzero if false (default), when no attempts have been made
1480 * '' is returned instead of 'Attempts: 0'.
1481 * @param int $currentgroup if there is a concept of current group where this method is being called
1482 * (e.g. a report) pass it in here. Default 0 which means no current group.
1483 * @return string HTML fragment for the link.
1485 function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false,
1486 $currentgroup = 0) {
1487 global $CFG;
1488 $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
1489 if (!$summary) {
1490 return '';
1493 require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1494 $url = new moodle_url('/mod/quiz/report.php', array(
1495 'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
1496 return html_writer::link($url, $summary);
1500 * @param string $feature FEATURE_xx constant for requested feature
1501 * @return bool True if quiz supports feature
1503 function quiz_supports($feature) {
1504 switch($feature) {
1505 case FEATURE_GROUPS: return true;
1506 case FEATURE_GROUPINGS: return true;
1507 case FEATURE_GROUPMEMBERSONLY: return true;
1508 case FEATURE_MOD_INTRO: return true;
1509 case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
1510 case FEATURE_GRADE_HAS_GRADE: return true;
1511 case FEATURE_GRADE_OUTCOMES: return false;
1512 case FEATURE_BACKUP_MOODLE2: return true;
1513 case FEATURE_SHOW_DESCRIPTION: return true;
1515 default: return null;
1520 * @return array all other caps used in module
1522 function quiz_get_extra_capabilities() {
1523 global $CFG;
1524 require_once($CFG->libdir.'/questionlib.php');
1525 $caps = question_get_all_capabilities();
1526 $caps[] = 'moodle/site:accessallgroups';
1527 return $caps;
1531 * This fucntion extends the global navigation for the site.
1532 * It is important to note that you should not rely on PAGE objects within this
1533 * body of code as there is no guarantee that during an AJAX request they are
1534 * available
1536 * @param navigation_node $quiznode The quiz node within the global navigation
1537 * @param object $course The course object returned from the DB
1538 * @param object $module The module object returned from the DB
1539 * @param object $cm The course module instance returned from the DB
1541 function quiz_extend_navigation($quiznode, $course, $module, $cm) {
1542 global $CFG;
1544 $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1546 if (has_capability('mod/quiz:view', $context)) {
1547 $url = new moodle_url('/mod/quiz/view.php', array('id'=>$cm->id));
1548 $quiznode->add(get_string('info', 'quiz'), $url, navigation_node::TYPE_SETTING,
1549 null, null, new pix_icon('i/info', ''));
1552 if (has_any_capability(array('mod/quiz:viewreports', 'mod/quiz:grade'), $context)) {
1553 require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php');
1554 $reportlist = quiz_report_list($context);
1556 $url = new moodle_url('/mod/quiz/report.php',
1557 array('id' => $cm->id, 'mode' => reset($reportlist)));
1558 $reportnode = $quiznode->add(get_string('results', 'quiz'), $url,
1559 navigation_node::TYPE_SETTING,
1560 null, null, new pix_icon('i/report', ''));
1562 foreach ($reportlist as $report) {
1563 $url = new moodle_url('/mod/quiz/report.php',
1564 array('id' => $cm->id, 'mode' => $report));
1565 $reportnode->add(get_string($report, 'quiz_'.$report), $url,
1566 navigation_node::TYPE_SETTING,
1567 null, 'quiz_report_' . $report, new pix_icon('i/item', ''));
1573 * This function extends the settings navigation block for the site.
1575 * It is safe to rely on PAGE here as we will only ever be within the module
1576 * context when this is called
1578 * @param settings_navigation $settings
1579 * @param navigation_node $quiznode
1581 function quiz_extend_settings_navigation($settings, $quiznode) {
1582 global $PAGE, $CFG;
1585 * Require {@link questionlib.php}
1586 * Included here as we only ever want to include this file if we really need to.
1588 require_once($CFG->libdir . '/questionlib.php');
1590 // We want to add these new nodes after the Edit settings node, and before the
1591 // Locally assigned roles node. Of course, both of those are controlled by capabilities.
1592 $keys = $quiznode->get_children_key_list();
1593 $beforekey = null;
1594 $i = array_search('modedit', $keys);
1595 if ($i === false and array_key_exists(0, $keys)) {
1596 $beforekey = $keys[0];
1597 } else if (array_key_exists($i + 1, $keys)) {
1598 $beforekey = $keys[$i + 1];
1601 if (has_capability('mod/quiz:manageoverrides', $PAGE->cm->context)) {
1602 $url = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$PAGE->cm->id));
1603 $node = navigation_node::create(get_string('groupoverrides', 'quiz'),
1604 new moodle_url($url, array('mode'=>'group')),
1605 navigation_node::TYPE_SETTING, null, 'mod_quiz_groupoverrides');
1606 $quiznode->add_node($node, $beforekey);
1608 $node = navigation_node::create(get_string('useroverrides', 'quiz'),
1609 new moodle_url($url, array('mode'=>'user')),
1610 navigation_node::TYPE_SETTING, null, 'mod_quiz_useroverrides');
1611 $quiznode->add_node($node, $beforekey);
1614 if (has_capability('mod/quiz:manage', $PAGE->cm->context)) {
1615 $node = navigation_node::create(get_string('editquiz', 'quiz'),
1616 new moodle_url('/mod/quiz/edit.php', array('cmid'=>$PAGE->cm->id)),
1617 navigation_node::TYPE_SETTING, null, 'mod_quiz_edit',
1618 new pix_icon('t/edit', ''));
1619 $quiznode->add_node($node, $beforekey);
1622 if (has_capability('mod/quiz:preview', $PAGE->cm->context)) {
1623 $url = new moodle_url('/mod/quiz/startattempt.php',
1624 array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey()));
1625 $node = navigation_node::create(get_string('preview', 'quiz'), $url,
1626 navigation_node::TYPE_SETTING, null, 'mod_quiz_preview',
1627 new pix_icon('t/preview', ''));
1628 $quiznode->add_node($node, $beforekey);
1631 question_extend_settings_navigation($quiznode, $PAGE->cm->context)->trim_if_empty();
1635 * Serves the quiz files.
1637 * @param object $course
1638 * @param object $cm
1639 * @param object $context
1640 * @param string $filearea
1641 * @param array $args
1642 * @param bool $forcedownload
1643 * @return bool false if file not found, does not return if found - justsend the file
1645 function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload) {
1646 global $CFG, $DB;
1648 if ($context->contextlevel != CONTEXT_MODULE) {
1649 return false;
1652 require_login($course, false, $cm);
1654 if (!$quiz = $DB->get_record('quiz', array('id'=>$cm->instance))) {
1655 return false;
1658 // 'intro' area is served by pluginfile.php
1659 $fileareas = array('feedback');
1660 if (!in_array($filearea, $fileareas)) {
1661 return false;
1664 $feedbackid = (int)array_shift($args);
1665 if (!$feedback = $DB->get_record('quiz_feedback', array('id'=>$feedbackid))) {
1666 return false;
1669 $fs = get_file_storage();
1670 $relativepath = implode('/', $args);
1671 $fullpath = "/$context->id/mod_quiz/$filearea/$feedbackid/$relativepath";
1672 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1673 return false;
1675 send_stored_file($file, 0, 0, true);
1679 * Called via pluginfile.php -> question_pluginfile to serve files belonging to
1680 * a question in a question_attempt when that attempt is a quiz attempt.
1682 * @param object $course course settings object
1683 * @param object $context context object
1684 * @param string $component the name of the component we are serving files for.
1685 * @param string $filearea the name of the file area.
1686 * @param array $args the remaining bits of the file path.
1687 * @param bool $forcedownload whether the user must be forced to download the file.
1688 * @return bool false if file not found, does not return if found - justsend the file
1690 function mod_quiz_question_pluginfile($course, $context, $component,
1691 $filearea, $qubaid, $slot, $args, $forcedownload) {
1692 global $CFG;
1693 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
1695 $attemptobj = quiz_attempt::create_from_usage_id($qubaid);
1696 require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
1698 if ($attemptobj->is_own_attempt() && !$attemptobj->is_finished()) {
1699 // In the middle of an attempt.
1700 if (!$attemptobj->is_preview_user()) {
1701 $attemptobj->require_capability('mod/quiz:attempt');
1703 $isreviewing = false;
1705 } else {
1706 // Reviewing an attempt.
1707 $attemptobj->check_review_capability();
1708 $isreviewing = true;
1711 if (!$attemptobj->check_file_access($slot, $isreviewing, $context->id,
1712 $component, $filearea, $args, $forcedownload)) {
1713 send_file_not_found();
1716 $fs = get_file_storage();
1717 $relativepath = implode('/', $args);
1718 $fullpath = "/$context->id/$component/$filearea/$relativepath";
1719 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1720 send_file_not_found();
1723 send_stored_file($file, 0, 0, $forcedownload);
1727 * Return a list of page types
1728 * @param string $pagetype current page type
1729 * @param stdClass $parentcontext Block's parent context
1730 * @param stdClass $currentcontext Current context of block
1732 function quiz_page_type_list($pagetype, $parentcontext, $currentcontext) {
1733 $module_pagetype = array(
1734 'mod-quiz-*'=>get_string('page-mod-quiz-x', 'quiz'),
1735 'mod-quiz-edit'=>get_string('page-mod-quiz-edit', 'quiz'));
1736 return $module_pagetype;