2d373a3173e40ffb483b52cd918999a7b47568d0
[moodle.git] / mod / quiz / lib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Library of functions for the quiz module.
20  *
21  * This contains functions that are called also from outside the quiz module
22  * Functions that are only called by the quiz module itself are in {@link locallib.php}
23  *
24  * @package mod-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
27  */
29 /** Require {@link eventslib.php} */
30 require_once($CFG->libdir . '/eventslib.php');
31 /** Require {@link calendar/lib.php} */
32 require_once($CFG->dirroot . '/calendar/lib.php');
34 /// CONSTANTS ///////////////////////////////////////////////////////////////////
36 /**#@+
37  * Options determining how the grades from individual attempts are combined to give
38  * the overall grade for a user
39  */
40 define('QUIZ_GRADEHIGHEST', 1);
41 define('QUIZ_GRADEAVERAGE', 2);
42 define('QUIZ_ATTEMPTFIRST', 3);
43 define('QUIZ_ATTEMPTLAST', 4);
44 /**#@-*/
46 define('QUIZ_MAX_ATTEMPT_OPTION', 10);
47 define('QUIZ_MAX_QPP_OPTION', 50);
48 define('QUIZ_MAX_DECIMAL_OPTION', 5);
49 define('QUIZ_MAX_Q_DECIMAL_OPTION', 7);
51 /**#@+
52  * The different review options are stored in the bits of $quiz->review
53  * These constants help to extract the options
54  *
55  * This is more of a mess than you might think necessary, because originally
56  * it was though that 3x6 bits were enough, but then they ran out. PHP integers
57  * are only reliably 32 bits signed, so the simplest solution was then to
58  * add 4x3 more bits.
59  */
60 /**
61  * The first 6 + 4 bits refer to the time immediately after the attempt
62  */
63 define('QUIZ_REVIEW_IMMEDIATELY', 0x3c003f);
64 /**
65  * the next 6 + 4 bits refer to the time after the attempt but while the quiz is open
66  */
67 define('QUIZ_REVIEW_OPEN',       0x3c00fc0);
68 /**
69  * the final 6 + 4 bits refer to the time after the quiz closes
70  */
71 define('QUIZ_REVIEW_CLOSED',    0x3c03f000);
73 // within each group of 6 bits we determine what should be shown
74 define('QUIZ_REVIEW_RESPONSES',       1*0x1041); // Show responses
75 define('QUIZ_REVIEW_SCORES',          2*0x1041); // Show scores
76 define('QUIZ_REVIEW_FEEDBACK',        4*0x1041); // Show question feedback
77 define('QUIZ_REVIEW_ANSWERS',         8*0x1041); // Show correct answers
78 // Some handling of worked solutions is already in the code but not yet fully supported
79 // and not switched on in the user interface.
80 define('QUIZ_REVIEW_SOLUTIONS',      16*0x1041); // Show solutions
81 define('QUIZ_REVIEW_GENERALFEEDBACK',32*0x1041); // Show question general feedback
82 define('QUIZ_REVIEW_OVERALLFEEDBACK', 1*0x4440000); // Show quiz overall feedback
83 // Multipliers 2*0x4440000, 4*0x4440000 and 8*0x4440000 are still available
84 /**#@-*/
86 /**
87  * If start and end date for the quiz are more than this many seconds apart
88  * they will be represented by two separate events in the calendar
89  */
90 define("QUIZ_MAX_EVENT_LENGTH", 5*24*60*60);   // 5 days maximum
92 /// FUNCTIONS ///////////////////////////////////////////////////////////////////
94 /**
95  * Given an object containing all the necessary data,
96  * (defined by the form in mod_form.php) this function
97  * will create a new instance and return the id number
98  * of the new instance.
99  *
100  * @global object
101  * @param object $quiz the data that came from the form.
102  * @return mixed the id of the new instance on success,
103  *          false or a string error message on failure.
104  */
105 function quiz_add_instance($quiz) {
106     global $DB;
108     // Process the options from the form.
109     $quiz->created = time();
110     $quiz->questions = '';
111     $result = quiz_process_options($quiz);
112     if ($result && is_string($result)) {
113         return $result;
114     }
116     // Try to store it in the database.
117     $quiz->id = $DB->insert_record('quiz', $quiz);
119     // Do the processing required after an add or an update.
120     quiz_after_add_or_update($quiz);
122     return $quiz->id;
125 /**
126  * Given an object containing all the necessary data,
127  * (defined by the form in mod_form.php) this function
128  * will update an existing instance with new data.
129  *
130  * @global stdClass
131  * @global object
132  * @param object $quiz the data that came from the form.
133  * @return mixed true on success, false or a string error message on failure.
134  */
135 function quiz_update_instance($quiz, $mform) {
136     global $CFG, $DB;
138     // Process the options from the form.
139     $result = quiz_process_options($quiz);
140     if ($result && is_string($result)) {
141         return $result;
142     }
144     // Repaginate, if asked to.
145     if (!$quiz->shufflequestions && !empty($quiz->repaginatenow)) {
146         require_once($CFG->dirroot . '/mod/quiz/locallib.php');
147         $quiz->questions = $DB->get_field('quiz', 'questions', array('id' => $quiz->instance));
148         $quiz->questions = quiz_repaginate($quiz->questions, $quiz->questionsperpage);
149     }
150     unset($quiz->repaginatenow);
152     // Update the database.
153     $quiz->id = $quiz->instance;
154     $DB->update_record('quiz', $quiz);
156     // Do the processing required after an add or an update.
157     quiz_after_add_or_update($quiz);
159     // Delete any previous preview attempts
160     quiz_delete_previews($quiz);
162     return true;
165 /**
166  * Given an ID of an instance of this module,
167  * this function will permanently delete the instance
168  * and any data that depends on it.
169  *
170  * @global object
171  * @param int $id
172  * @return bool
173  */
174 function quiz_delete_instance($id) {
175     global $DB;
177     if (!$quiz = $DB->get_record('quiz', array('id' => $id))) {
178         return false;
179     }
181     quiz_delete_all_attempts($quiz);
182     quiz_delete_all_overrides($quiz);
184     $DB->delete_records('quiz_question_instances', array('quiz' => $quiz->id));
185     $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
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     }
193     quiz_grade_item_delete($quiz);
194     $DB->delete_records('quiz', array('id' => $quiz->id));
196     return true;
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 integer $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;
209     if (!$override = $DB->get_record('quiz_overrides', array('id' => $overrideid))) {
210         return false;
211     }
212     $groupid   = empty($override->groupid)?   0 : $override->groupid;
213     $userid    = empty($override->userid)?    0 : $override->userid;
215     // Delete the events
216     $events = $DB->get_records('event', array('modulename'=>'quiz', 'instance'=>$quiz->id, 'groupid'=>$groupid, 'userid'=>$userid));
217     foreach($events as $event) {
218         $eventold = calendar_event::load($event);
219         $eventold->delete();
220     }
222     $DB->delete_records('quiz_overrides', array('id' => $overrideid));
223     return true;
226 /**
227  * Deletes all quiz overrides from the database and clears any corresponding calendar events
228  *
229  * @param object $quiz The quiz object.
230  */
231 function quiz_delete_all_overrides($quiz) {
232     global $DB;
234     $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id');
235     foreach ($overrides as $override) {
236         quiz_delete_override($quiz, $override->id);
237     }
240 /**
241  * Updates a quiz object with override information for a user.
242  *
243  * Algorithm:  For each quiz setting, if there is a matching user-specific override,
244  *   then use that otherwise, if there are group-specific overrides, return the most
245  *   lenient combination of them.  If neither applies, leave the quiz setting unchanged.
246  *
247  *   Special case: if there is more than one password that applies to the user, then
248  *   quiz->extrapasswords will contain an array of strings giving the remaining
249  *   passwords.
250  *
251  * @param object $quiz The quiz object.
252  * @param integer $userid The userid.
253  * @return object $quiz The updated quiz object.
254  */
255 function quiz_update_effective_access($quiz, $userid) {
256     global $DB;
258     // check for user override
259     $override = $DB->get_record('quiz_overrides', array('quiz' => $quiz->id, 'userid' => $userid));
261     if (!$override) {
262         $override = new stdclass;
263         $override->timeopen = null;
264         $override->timeclose = null;
265         $override->timelimit = null;
266         $override->attempts = null;
267         $override->password = null;
268     }
270     // check for group overrides
271     $groupings = groups_get_user_groups($quiz->course, $userid);
273     if (!empty($groupings[0])) {
274         // Select all overrides that apply to the User's groups
275         list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
276         $sql = "SELECT * FROM {quiz_overrides}
277                 WHERE groupid $extra AND quiz = ?";
278         $params[] = $quiz->id;
279         $records = $DB->get_records_sql($sql, $params);
281         // Combine the overrides
282         $opens = array();
283         $closes = array();
284         $limits = array();
285         $attempts = array();
286         $passwords = array();
288         foreach ($records as $gpoverride) {
289             if (isset($gpoverride->timeopen)) {
290                 $opens[] = $gpoverride->timeopen;
291             }
292             if (isset($gpoverride->timeclose)) {
293                 $closes[] = $gpoverride->timeclose;
294             }
295             if (isset($gpoverride->timelimit)) {
296                 $limits[] = $gpoverride->timelimit;
297             }
298             if (isset($gpoverride->attempts)) {
299                 $attempts[] = $gpoverride->attempts;
300             }
301             if (isset($gpoverride->password)) {
302                 $passwords[] = $gpoverride->password;
303             }
304         }
305         // If there is a user override for a setting, ignore the group override
306         if (is_null($override->timeopen) && count($opens)) {
307             $override->timeopen  = min($opens);
308         }
309         if (is_null($override->timeclose) && count($closes)) {
310             $override->timeclose  = max($closes);
311         }
312         if (is_null($override->timelimit) && count($limits)) {
313             $override->timelimit  = max($limits);
314         }
315         if (is_null($override->attempts) && count($attempts)) {
316             $override->attempts  = max($attempts);
317         }
318         if (is_null($override->password) && count($passwords)) {
319             $override->password  = array_shift($passwords);
320             if (count($passwords)) {
321                 $override->extrapasswords  = $passwords;
322             }
323         }
325     }
327     // merge with quiz defaults
328     $keys = array('timeopen','timeclose', 'timelimit', 'attempts', 'password', 'extrapasswords');
329     foreach ($keys as $key) {
330         if (isset($override->{$key})) {
331             $quiz->{$key} = $override->{$key};
332         }
333     }
335     return $quiz;
338 /**
339  * Delete all the attempts belonging to a quiz.
340  *
341  * @global stdClass
342  * @global object
343  * @param object $quiz The quiz object.
344  */
345 function quiz_delete_all_attempts($quiz) {
346     global $CFG, $DB;
347     require_once($CFG->libdir . '/questionlib.php');
348     $attempts = $DB->get_records('quiz_attempts', array('quiz' => $quiz->id));
349     foreach ($attempts as $attempt) {
350         delete_attempt($attempt->uniqueid);
351     }
352     $DB->delete_records('quiz_attempts', array('quiz' => $quiz->id));
353     $DB->delete_records('quiz_grades', array('quiz' => $quiz->id));
356 /**
357  * Return a small object with summary information about what a
358  * user has done with a given particular instance of this module
359  * Used for user activity reports.
360  * $return->time = the time they did it
361  * $return->info = a short text description
362  *
363  * @global object
364  * @param object $course
365  * @param object $user
366  * @param object $mod
367  * @param object $quiz
368  * @return object|null
369  */
370 function quiz_user_outline($course, $user, $mod, $quiz) {
371     global $DB, $CFG;
372     require_once("$CFG->libdir/gradelib.php");
373     $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
375     if (empty($grades->items[0]->grades)) {
376         return null;
377     } else {
378         $grade = reset($grades->items[0]->grades);
379     }
381     $result = new stdClass;
382     $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
383     $result->time = $grade->dategraded;
384     return $result;
387 /**
388  * Is this a graded quiz? If this method returns true, you can assume that
389  * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to
390  * divide by them).
391  *
392  * @param object $quiz a row from the quiz table.
393  * @return boolean whether this is a graded quiz.
394  */
395 function quiz_has_grades($quiz) {
396     return $quiz->grade != 0 && $quiz->sumgrades != 0;
399 /**
400  * Get the best current grade for a particular user in a quiz.
401  *
402  * @param object $quiz the quiz settings.
403  * @param integer $userid the id of the user.
404  * @return float the user's current grade for this quiz, or NULL if this user does
405  * not have a grade on this quiz.
406  */
407 function quiz_get_best_grade($quiz, $userid) {
408     global $DB;
409     $grade = $DB->get_field('quiz_grades', 'grade', array('quiz' => $quiz->id, 'userid' => $userid));
411     // Need to detect errors/no result, without catching 0 scores.
412     if ($grade === false) {
413         return null;
414     }
416     return $grade + 0; // Convert to number.
419 /**
420  * Print a detailed representation of what a  user has done with
421  * a given particular instance of this module, for user activity reports.
422  *
423  * @global object
424  * @param object $course
425  * @param object $user
426  * @param object $mod
427  * @param object $quiz
428  * @return bool
429  */
430 function quiz_user_complete($course, $user, $mod, $quiz) {
431     global $DB, $CFG, $OUTPUT;
432     require_once("$CFG->libdir/gradelib.php");
433     $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
434     if (!empty($grades->items[0]->grades)) {
435         $grade = reset($grades->items[0]->grades);
436         echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
437         if ($grade->str_feedback) {
438             echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
439         }
440     }
442     if ($attempts = $DB->get_records('quiz_attempts', array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) {
443         foreach ($attempts as $attempt) {
444             echo get_string('attempt', 'quiz').' '.$attempt->attempt.': ';
445             if ($attempt->timefinish == 0) {
446                 print_string('unfinished');
447             } else {
448                 echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' . quiz_format_grade($quiz, $quiz->sumgrades);
449             }
450             echo ' - '.userdate($attempt->timemodified).'<br />';
451         }
452     } else {
453        print_string('noattempts', 'quiz');
454     }
456     return true;
459 /**
460  * Function to be run periodically according to the moodle cron
461  * This function searches for things that need to be done, such
462  * as sending out mail, toggling flags etc ...
463  *
464  * @global stdClass
465  * @return bool true
466  */
467 function quiz_cron() {
468     global $CFG;
470     return true;
473 /**
474  * @global object
475  * @param integer $quizid the quiz id.
476  * @param integer $userid the userid.
477  * @param string $status 'all', 'finished' or 'unfinished' to control
478  * @param bool $includepreviews
479  * @return an array of all the user's attempts at this quiz. Returns an empty array if there are none.
480  */
481 function quiz_get_user_attempts($quizid, $userid=0, $status = 'finished', $includepreviews = false) {
482     global $DB;
483     $status_condition = array(
484         'all' => '',
485         'finished' => ' AND timefinish > 0',
486         'unfinished' => ' AND timefinish = 0'
487     );
488     $previewclause = '';
489     if (!$includepreviews) {
490         $previewclause = ' AND preview = 0';
491     }
492     $params=array($quizid);
493     if ($userid){
494         $userclause = ' AND userid = ?';
495         $params[]=$userid;
496     } else {
497         $userclause = '';
498     }
499     if ($attempts = $DB->get_records_select('quiz_attempts',
500             "quiz = ?" .$userclause. $previewclause . $status_condition[$status], $params,
501             'attempt ASC')) {
502         return $attempts;
503     } else {
504         return array();
505     }
508 /**
509  * Return grade for given user or all users.
510  *
511  * @global stdClass
512  * @global object
513  * @param int $quizid id of quiz
514  * @param int $userid optional user id, 0 means all users
515  * @return array array of grades, false if none. These are raw grades. They should
516  * be processed with quiz_format_grade for display.
517  */
518 function quiz_get_user_grades($quiz, $userid=0) {
519     global $CFG, $DB;
521     $params = array($quiz->id);
522     $wheresql = '';
523     if ($userid) {
524         $params[] = $userid;
525         $wheresql = "AND u.id = ?";
526     }
527     $sql = "SELECT u.id, u.id AS userid, g.grade AS rawgrade, g.timemodified AS dategraded, MAX(a.timefinish) AS datesubmitted
528             FROM {user} u, {quiz_grades} g, {quiz_attempts} a
529             WHERE u.id = g.userid AND g.quiz = ? AND a.quiz = g.quiz AND u.id = a.userid $wheresql
530             GROUP BY u.id, g.grade, g.timemodified";
532     return $DB->get_records_sql($sql, $params);
535 /**
536  * Round a grade to to the correct number of decimal places, and format it for display.
537  *
538  * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
539  * @param float $grade The grade to round.
540  * @return float
541  */
542 function quiz_format_grade($quiz, $grade) {
543     return format_float($grade, $quiz->decimalpoints);
546 /**
547  * Round a grade to to the correct number of decimal places, and format it for display.
548  *
549  * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
550  * @param float $grade The grade to round.
551  * @return float
552  */
553 function quiz_format_question_grade($quiz, $grade) {
554     if ($quiz->questiondecimalpoints == -1) {
555         return format_float($grade, $quiz->decimalpoints);
556     } else {
557         return format_float($grade, $quiz->questiondecimalpoints);
558     }
561 /**
562  * Update grades in central gradebook
563  *
564  * @global stdClass
565  * @global object
566  * @param object $quiz
567  * @param int $userid specific user only, 0 means all
568  */
569 function quiz_update_grades($quiz, $userid=0, $nullifnone=true) {
570     global $CFG, $DB;
571     require_once($CFG->libdir.'/gradelib.php');
573     if ($quiz->grade == 0) {
574         quiz_grade_item_update($quiz);
576     } else if ($grades = quiz_get_user_grades($quiz, $userid)) {
577         quiz_grade_item_update($quiz, $grades);
579     } else if ($userid and $nullifnone) {
580         $grade = new object();
581         $grade->userid   = $userid;
582         $grade->rawgrade = NULL;
583         quiz_grade_item_update($quiz, $grade);
585     } else {
586         quiz_grade_item_update($quiz);
587     }
590 /**
591  * Update all grades in gradebook.
592  *
593  * @global object
594  */
595 function quiz_upgrade_grades() {
596     global $DB;
598     $sql = "SELECT COUNT('x')
599               FROM {quiz} a, {course_modules} cm, {modules} m
600              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
601     $count = $DB->count_records_sql($sql);
603     $sql = "SELECT a.*, cm.idnumber AS cmidnumber, a.course AS courseid
604               FROM {quiz} a, {course_modules} cm, {modules} m
605              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
606     if ($rs = $DB->get_recordset_sql($sql)) {
607         $pbar = new progress_bar('quizupgradegrades', 500, true);
608         $i=0;
609         foreach ($rs as $quiz) {
610             $i++;
611             upgrade_set_timeout(60*5); // set up timeout, may also abort execution
612             quiz_update_grades($quiz, 0, false);
613             $pbar->update($i, $count, "Updating Quiz grades ($i/$count).");
614         }
615         $rs->close();
616     }
619 /**
620  * Create grade item for given quiz
621  *
622  * @global stdClass
623  * @uses GRADE_TYPE_VALUE
624  * @uses GRADE_TYPE_NONE
625  * @uses QUIZ_REVIEW_SCORES
626  * @uses QUIZ_REVIEW_CLOSED
627  * @uses QUIZ_REVIEW_OPEN
628  * @uses PARAM_INT
629  * @uses GRADE_UPDATE_ITEM_LOCKED
630  * @param object $quiz object with extra cmidnumber
631  * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
632  * @return int 0 if ok, error code otherwise
633  */
634 function quiz_grade_item_update($quiz, $grades=NULL) {
635     global $CFG, $OUTPUT;
636     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
637         require_once($CFG->libdir.'/gradelib.php');
638     }
640     if (array_key_exists('cmidnumber', $quiz)) { //it may not be always present
641         $params = array('itemname'=>$quiz->name, 'idnumber'=>$quiz->cmidnumber);
642     } else {
643         $params = array('itemname'=>$quiz->name);
644     }
646     if ($quiz->grade > 0) {
647         $params['gradetype'] = GRADE_TYPE_VALUE;
648         $params['grademax']  = $quiz->grade;
649         $params['grademin']  = 0;
651     } else {
652         $params['gradetype'] = GRADE_TYPE_NONE;
653     }
655 /* description by TJ:
656 1/ If the quiz is set to not show scores while the quiz is still open, and is set to show scores after
657    the quiz is closed, then create the grade_item with a show-after date that is the quiz close date.
658 2/ If the quiz is set to not show scores at either of those times, create the grade_item as hidden.
659 3/ If the quiz is set to show scores, create the grade_item visible.
660 */
661     if (!($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
662     and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
663         $params['hidden'] = 1;
665     } else if ( ($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
666            and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
667         if ($quiz->timeclose) {
668             $params['hidden'] = $quiz->timeclose;
669         } else {
670             $params['hidden'] = 1;
671         }
673     } else {
674         // a) both open and closed enabled
675         // b) open enabled, closed disabled - we can not "hide after", grades are kept visible even after closing
676         $params['hidden'] = 0;
677     }
679     if ($grades  === 'reset') {
680         $params['reset'] = true;
681         $grades = NULL;
682     }
684     $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
685     if (!empty($gradebook_grades->items)) {
686         $grade_item = $gradebook_grades->items[0];
687         if ($grade_item->locked) {
688             $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
689             if (!$confirm_regrade) {
690                 $message = get_string('gradeitemislocked', 'grades');
691                 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id . '&amp;mode=overview';
692                 $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
693                 echo $OUTPUT->box_start('generalbox', 'notice');
694                 echo '<p>'. $message .'</p>';
695                 echo $OUTPUT->container_start('buttons');
696                 echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades'));
697                 echo $OUTPUT->single_button($back_link,  get_string('cancel'));
698                 echo $OUTPUT->container_end();
699                 echo $OUTPUT->box_end();
701                 return GRADE_UPDATE_ITEM_LOCKED;
702             }
703         }
704     }
706     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
709 /**
710  * Delete grade item for given quiz
711  *
712  * @global stdClass
713  * @param object $quiz object
714  * @return object quiz
715  */
716 function quiz_grade_item_delete($quiz) {
717     global $CFG;
718     require_once($CFG->libdir . '/gradelib.php');
720     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, NULL, array('deleted' => 1));
723 /**
724  * @return the options for calculating the quiz grade from the individual attempt grades.
725  */
726 function quiz_get_grading_options() {
727     return array (
728             QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'),
729             QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'),
730             QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'),
731             QUIZ_ATTEMPTLAST  => get_string('attemptlast', 'quiz'));
734 /**
735  * Returns an array of users who have data in a given quiz
736  *
737  * @global stdClass
738  * @global object
739  * @param int $quizid
740  * @return array
741  */
742 function quiz_get_participants($quizid) {
743     global $CFG, $DB;
745     //Get users from attempts
746     $us_attempts = $DB->get_records_sql("SELECT DISTINCT u.id, u.id
747                                     FROM {user} u,
748                                          {quiz_attempts} a
749                                     WHERE a.quiz = ? and
750                                           u.id = a.userid", array($quizid));
752     //Return us_attempts array (it contains an array of unique users)
753     return $us_attempts;
757 /**
758  * This standard function will check all instances of this module
759  * and make sure there are up-to-date events created for each of them.
760  * If courseid = 0, then every quiz event in the site is checked, else
761  * only quiz events belonging to the course specified are checked.
762  * This function is used, in its new format, by restore_refresh_events()
763  *
764  * @global object
765  * @uses QUIZ_MAX_EVENT_LENGTH
766  * @param int $courseid
767  * @return bool
768  */
769 function quiz_refresh_events($courseid = 0) {
770     global $DB;
772     if ($courseid == 0) {
773         if (! $quizzes = $DB->get_records('quiz')) {
774             return true;
775         }
776     } else {
777         if (! $quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
778             return true;
779         }
780     }
782     foreach ($quizzes as $quiz) {
783         quiz_update_events($quiz);
784     }
786     return true;
789 /**
790  * Returns all quiz graded users since a given time for specified quiz
791  */
792 function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
793         $courseid, $cmid, $userid = 0, $groupid = 0) {
794     global $CFG, $COURSE, $USER, $DB;
795     require_once('locallib.php');
797     if ($COURSE->id == $courseid) {
798         $course = $COURSE;
799     } else {
800         $course = $DB->get_record('course', array('id' => $courseid));
801     }
803     $modinfo =& get_fast_modinfo($course);
805     $cm = $modinfo->cms[$cmid];
806     $quiz = $DB->get_record('quiz', array('id' => $cm->instance));
808     if ($userid) {
809         $userselect = "AND u.id = :userid";
810         $params['userid'] = $userid;
811     } else {
812         $userselect = '';
813     }
815     if ($groupid) {
816         $groupselect = 'AND gm.groupid = :groupid';
817         $groupjoin   = 'JOIN {groups_members} gm ON  gm.userid=u.id';
818         $params['groupid'] = $groupid;
819     } else {
820         $groupselect = '';
821         $groupjoin   = '';
822     }
824     $params['timestart'] = $timestart;
825     $params['quizid'] = $quiz->id;
827     if (!$attempts = $DB->get_records_sql("
828               SELECT qa.*,
829                      u.firstname, u.lastname, u.email, u.picture, u.imagealt
830                 FROM {quiz_attempts} qa
831                      JOIN {user} u ON u.id = qa.userid
832                      $groupjoin
833                WHERE qa.timefinish > :timestart
834                  AND qa.quiz = :quizid
835                  AND qa.preview = 0
836                      $userselect
837                      $groupselect
838             ORDER BY qa.timefinish ASC", $params)) {
839         return;
840     }
842     $context         = get_context_instance(CONTEXT_MODULE, $cm->id);
843     $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
844     $viewfullnames   = has_capability('moodle/site:viewfullnames', $context);
845     $grader          = has_capability('mod/quiz:viewreports', $context);
846     $groupmode       = groups_get_activity_groupmode($cm, $course);
848     if (is_null($modinfo->groups)) {
849         $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
850     }
852     $usersgroups = null;
853     $aname = format_string($cm->name,true);
854     foreach ($attempts as $attempt) {
855         if ($attempt->userid != $USER->id) {
856             if (!$grader) {
857                 // Grade permission required
858                 continue;
859             }
861             if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
862                 if (is_null($usersgroups)) {
863                     $usersgroups = groups_get_all_groups($course->id,
864                             $attempt->userid, $cm->groupingid);
865                     if (is_array($usersgroups)) {
866                         $usersgroups = array_keys($usersgroups);
867                     } else {
868                         $usersgroups = array();
869                     }
870                 }
871                 if (!array_intersect($usersgroups, $modinfo->groups[$cm->id])) {
872                     continue;
873                 }
874             }
875         }
877         $options = quiz_get_reviewoptions($quiz, $attempt, $context);
879         $tmpactivity = new stdClass;
881         $tmpactivity->type       = 'quiz';
882         $tmpactivity->cmid       = $cm->id;
883         $tmpactivity->name       = $aname;
884         $tmpactivity->sectionnum = $cm->sectionnum;
885         $tmpactivity->timestamp  = $attempt->timefinish;
887         $tmpactivity->content->attemptid = $attempt->id;
888         $tmpactivity->content->attempt   = $attempt->attempt;
889         if (quiz_has_grades($quiz) && $options->scores) {
890             $tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades);
891             $tmpactivity->content->maxgrade  = quiz_format_grade($quiz, $quiz->sumgrades);
892         } else {
893             $tmpactivity->content->sumgrades = null;
894             $tmpactivity->content->maxgrade  = null;
895         }
897         $tmpactivity->user->id   = $attempt->id;
898         $tmpactivity->user->firstname = $attempt->firstname;
899         $tmpactivity->user->lastname = $attempt->lastname;
900         $tmpactivity->user->fullname = fullname($attempt, $viewfullnames);
901         $tmpactivity->user->picture  = $attempt->picture;
902         $tmpactivity->user->imagealt = $attempt->imagealt;
903         $tmpactivity->user->email = $attempt->email;
905         $activities[$index++] = $tmpactivity;
906     }
908   return;
911 function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
912     global $CFG, $OUTPUT;
914     echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">';
916     echo '<tr><td class="userpicture" valign="top">';
917     echo $OUTPUT->user_picture($activity->user, array('courseid' => $courseid));
918     echo '</td><td>';
920     if ($detail) {
921         $modname = $modnames[$activity->type];
922         echo '<div class="title">';
923         echo '<img src="' . $OUTPUT->pix_url('icon', $activity->type) . '" ' .
924                 'class="icon" alt="' . $modname . '" />';
925         echo '<a href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
926                 $activity->cmid . '">' . $activity->name . '</a>';
927         echo '</div>';
928     }
930     echo '<div class="grade">';
931     echo  get_string('attempt', 'quiz', $activity->content->attempt);
932     if (isset($activity->content->maxgrade)) {
933         $grades = $activity->content->sumgrades . ' / ' . $activity->content->maxgrade;
934         echo ': (<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?attempt=' .
935                 $activity->content->attemptid . '">' . $grades . '</a>)';
936     }
937     echo '</div>';
939     echo '<div class="user">';
940     echo '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $activity->user->id .
941             '&amp;course=' . $courseid . '">' . $activity->user->fullname .
942             '</a> - ' . userdate($activity->timestamp);
943     echo '</div>';
945     echo '</td></tr></table>';
947     return;
950 /**
951  * Pre-process the quiz options form data, making any necessary adjustments.
952  * Called by add/update instance in this file.
953  *
954  * @uses QUIZ_REVIEW_OVERALLFEEDBACK
955  * @uses QUIZ_REVIEW_CLOSED
956  * @uses QUIZ_REVIEW_OPEN
957  * @uses QUIZ_REVIEW_IMMEDIATELY
958  * @uses QUIZ_REVIEW_GENERALFEEDBACK
959  * @uses QUIZ_REVIEW_SOLUTIONS
960  * @uses QUIZ_REVIEW_ANSWERS
961  * @uses QUIZ_REVIEW_FEEDBACK
962  * @uses QUIZ_REVIEW_SCORES
963  * @uses QUIZ_REVIEW_RESPONSES
964  * @uses QUESTION_ADAPTIVE
965  * @param object $quiz The variables set on the form.
966  * @return string
967  */
968 function quiz_process_options(&$quiz) {
969     $quiz->timemodified = time();
971     // Quiz name.
972     if (!empty($quiz->name)) {
973         $quiz->name = trim($quiz->name);
974     }
976     // Password field - different in form to stop browsers that remember passwords
977     // getting confused.
978     $quiz->password = $quiz->quizpassword;
979     unset($quiz->quizpassword);
981     // Quiz feedback
982     if (isset($quiz->feedbacktext)) {
983         // Clean up the boundary text.
984         for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
985             if (empty($quiz->feedbacktext[$i])) {
986                 $quiz->feedbacktext[$i] = '';
987             } else {
988                 $quiz->feedbacktext[$i] = trim($quiz->feedbacktext[$i]);
989             }
990         }
992         // Check the boundary value is a number or a percentage, and in range.
993         $i = 0;
994         while (!empty($quiz->feedbackboundaries[$i])) {
995             $boundary = trim($quiz->feedbackboundaries[$i]);
996             if (!is_numeric($boundary)) {
997                 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
998                     $boundary = trim(substr($boundary, 0, -1));
999                     if (is_numeric($boundary)) {
1000                         $boundary = $boundary * $quiz->grade / 100.0;
1001                     } else {
1002                         return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
1003                     }
1004                 }
1005             }
1006             if ($boundary <= 0 || $boundary >= $quiz->grade) {
1007                 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
1008             }
1009             if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
1010                 return get_string('feedbackerrororder', 'quiz', $i + 1);
1011             }
1012             $quiz->feedbackboundaries[$i] = $boundary;
1013             $i += 1;
1014         }
1015         $numboundaries = $i;
1017         // Check there is nothing in the remaining unused fields.
1018         if (!empty($quiz->feedbackboundaries)) {
1019             for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
1020                 if (!empty($quiz->feedbackboundaries[$i]) && trim($quiz->feedbackboundaries[$i]) != '') {
1021                     return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
1022                 }
1023             }
1024         }
1025         for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
1026             if (!empty($quiz->feedbacktext[$i]) && trim($quiz->feedbacktext[$i]) != '') {
1027                 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
1028             }
1029         }
1030         $quiz->feedbackboundaries[-1] = $quiz->grade + 1; // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
1031         $quiz->feedbackboundaries[$numboundaries] = 0;
1032         $quiz->feedbackboundarycount = $numboundaries;
1033     }
1035     // Settings that get combined to go into the optionflags column.
1036     $quiz->optionflags = 0;
1037     if (!empty($quiz->adaptive)) {
1038         $quiz->optionflags |= QUESTION_ADAPTIVE;
1039     }
1041     // Settings that get combined to go into the review column.
1042     $review = 0;
1043     if (isset($quiz->responsesimmediately)) {
1044         $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY);
1045         unset($quiz->responsesimmediately);
1046     }
1047     if (isset($quiz->responsesopen)) {
1048         $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN);
1049         unset($quiz->responsesopen);
1050     }
1051     if (isset($quiz->responsesclosed)) {
1052         $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED);
1053         unset($quiz->responsesclosed);
1054     }
1056     if (isset($quiz->scoreimmediately)) {
1057         $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY);
1058         unset($quiz->scoreimmediately);
1059     }
1060     if (isset($quiz->scoreopen)) {
1061         $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN);
1062         unset($quiz->scoreopen);
1063     }
1064     if (isset($quiz->scoreclosed)) {
1065         $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED);
1066         unset($quiz->scoreclosed);
1067     }
1069     if (isset($quiz->feedbackimmediately)) {
1070         $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1071         unset($quiz->feedbackimmediately);
1072     }
1073     if (isset($quiz->feedbackopen)) {
1074         $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN);
1075         unset($quiz->feedbackopen);
1076     }
1077     if (isset($quiz->feedbackclosed)) {
1078         $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED);
1079         unset($quiz->feedbackclosed);
1080     }
1082     if (isset($quiz->answersimmediately)) {
1083         $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
1084         unset($quiz->answersimmediately);
1085     }
1086     if (isset($quiz->answersopen)) {
1087         $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN);
1088         unset($quiz->answersopen);
1089     }
1090     if (isset($quiz->answersclosed)) {
1091         $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED);
1092         unset($quiz->answersclosed);
1093     }
1095     if (isset($quiz->solutionsimmediately)) {
1096         $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_IMMEDIATELY);
1097         unset($quiz->solutionsimmediately);
1098     }
1099     if (isset($quiz->solutionsopen)) {
1100         $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_OPEN);
1101         unset($quiz->solutionsopen);
1102     }
1103     if (isset($quiz->solutionsclosed)) {
1104         $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_CLOSED);
1105         unset($quiz->solutionsclosed);
1106     }
1108     if (isset($quiz->generalfeedbackimmediately)) {
1109         $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1110         unset($quiz->generalfeedbackimmediately);
1111     }
1112     if (isset($quiz->generalfeedbackopen)) {
1113         $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_OPEN);
1114         unset($quiz->generalfeedbackopen);
1115     }
1116     if (isset($quiz->generalfeedbackclosed)) {
1117         $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_CLOSED);
1118         unset($quiz->generalfeedbackclosed);
1119     }
1121     if (isset($quiz->overallfeedbackimmediately)) {
1122         $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1123         unset($quiz->overallfeedbackimmediately);
1124     }
1125     if (isset($quiz->overallfeedbackopen)) {
1126         $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_OPEN);
1127         unset($quiz->overallfeedbackopen);
1128     }
1129     if (isset($quiz->overallfeedbackclosed)) {
1130         $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_CLOSED);
1131         unset($quiz->overallfeedbackclosed);
1132     }
1134     $quiz->review = $review;
1137 /**
1138  * This function is called at the end of quiz_add_instance
1139  * and quiz_update_instance, to do the common processing.
1140  *
1141  * @global object
1142  * @uses QUIZ_MAX_EVENT_LENGTH
1143  * @param object $quiz the quiz object.
1144  * @return void|string Void or error message
1145  */
1146 function quiz_after_add_or_update($quiz) {
1147     global $DB;
1149     // Save the feedback
1150     $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
1152     for ($i = 0; $i <= $quiz->feedbackboundarycount; $i += 1) {
1153         $feedback = new stdClass;
1154         $feedback->quizid = $quiz->id;
1155         $feedback->feedbacktext = $quiz->feedbacktext[$i];
1156         $feedback->mingrade = $quiz->feedbackboundaries[$i];
1157         $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
1158         $DB->insert_record('quiz_feedback', $feedback, false);
1159     }
1161     // Update the events relating to this quiz.
1162     quiz_update_events($quiz);
1164     //update related grade item
1165     quiz_grade_item_update($quiz);
1169 /**
1170  * This function updates the events associated to the quiz.
1171  * If $override is non-zero, then it updates only the events
1172  * associated with the specified override.
1173  *
1174  * @uses QUIZ_MAX_EVENT_LENGTH
1175  * @param object $quiz the quiz object.
1176  * @param object optional $override limit to a specific override
1177  */
1178 function quiz_update_events($quiz, $override = null) {
1179     global $DB;
1181     // Load the old events relating to this quiz.
1182     $conds = array('modulename'=>'quiz',
1183                    'instance'=>$quiz->id);
1184     if (!empty($override)) {
1185         // only load events for this override
1186         $conds['groupid'] = isset($override->groupid)?  $override->groupid : 0;
1187         $conds['userid'] = isset($override->userid)?  $override->userid : 0;
1188     }
1189     $oldevents = $DB->get_records('event', $conds);
1191     // Now make a todo list of all that needs to be updated
1192     if (empty($override)) {
1193         // We are updating the primary settings for the quiz, so we
1194         // need to add all the overrides
1195         $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id));
1196         // as well as the original quiz (empty override)
1197         $overrides[] = new stdClass;
1198     }
1199     else {
1200         // Just do the one override
1201         $overrides = array($override);
1202     }
1204     foreach ($overrides as $current) {
1205         $groupid   = isset($current->groupid)?  $current->groupid : 0;
1206         $userid    = isset($current->userid)? $current->userid : 0;
1207         $timeopen  = isset($current->timeopen)?  $current->timeopen : $quiz->timeopen;
1208         $timeclose = isset($current->timeclose)? $current->timeclose : $quiz->timeclose;
1210         // only add open/close events for an override if they differ from the quiz default
1211         $addopen  = empty($current->id) || !empty($current->timeopen);
1212         $addclose = empty($current->id) || !empty($current->timeclose);
1214         $event = new stdClass;
1215         $event->description = $quiz->intro;
1216         $event->courseid    = ($userid) ? 0 : $quiz->course; // Events module won't show user events when the courseid is nonzero
1217         $event->groupid     = $groupid;
1218         $event->userid      = $userid;
1219         $event->modulename  = 'quiz';
1220         $event->instance    = $quiz->id;
1221         $event->timestart   = $timeopen;
1222         $event->timeduration = max($timeclose - $timeopen, 0);
1223         $event->visible     = instance_is_visible('quiz', $quiz);
1224         $event->eventtype   = 'open';
1226         // Determine the event name
1227         if ($groupid) {
1228             $params = new stdClass;
1229             $params->quiz = $quiz->name;
1230             $params->group = groups_get_group_name($groupid);
1231             if ($params->group === false) {
1232                 // group doesn't exist, just skip it
1233                 continue;
1234             }
1235             $eventname = get_string('overridegroupeventname', 'quiz', $params);
1236         }
1237         else if ($userid) {
1238             $params = new stdClass;
1239             $params->quiz = $quiz->name;
1240             $eventname = get_string('overrideusereventname', 'quiz', $params);
1241         } else {
1242             $eventname = $quiz->name;
1243         }
1244         if ($addopen or $addclose) {
1245             if ($timeclose and $timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) {
1246                 // Single event for the whole quiz.
1247                 if ($oldevent = array_shift($oldevents)) {
1248                     $event->id = $oldevent->id;
1249                 }
1250                 else {
1251                     unset($event->id);
1252                 }
1253                 $event->name = $eventname;
1254                 // calendar_event::create will reuse a db record if the id field is set
1255                 calendar_event::create($event);
1256             } else {
1257                 // Separate start and end events.
1258                 $event->timeduration  = 0;
1259                 if ($timeopen && $addopen) {
1260                     if ($oldevent = array_shift($oldevents)) {
1261                         $event->id = $oldevent->id;
1262                     }
1263                     else {
1264                         unset($event->id);
1265                     }
1266                     $event->name = $eventname.' ('.get_string('quizopens', 'quiz').')';
1267                     // calendar_event::create will reuse a db record if the id field is set
1268                     calendar_event::create($event);
1269                 }
1270                 if ($timeclose && $addclose) {
1271                     if ($oldevent = array_shift($oldevents)) {
1272                         $event->id = $oldevent->id;
1273                     }
1274                     else {
1275                         unset($event->id);
1276                     }
1277                     $event->name      = $eventname.' ('.get_string('quizcloses', 'quiz').')';
1278                     $event->timestart = $timeclose;
1279                     $event->eventtype = 'close';
1280                     calendar_event::create($event);
1281                 }
1282             }
1283         }
1284     }
1286     // Delete any leftover events
1287     foreach ($oldevents as $badevent) {
1288         $badevent = calendar_event::load($badevent);
1289         $badevent->delete();
1290     }
1293 /**
1294  * @return array
1295  */
1296 function quiz_get_view_actions() {
1297     return array('view', 'view all', 'report', 'review');
1300 /**
1301  * @return array
1302  */
1303 function quiz_get_post_actions() {
1304     return array('attempt', 'close attempt', 'preview', 'editquestions', 'delete attempt', 'manualgrade');
1307 /**
1308  * Returns an array of names of quizzes that use this question
1309  *
1310  * @param integer $questionid
1311  * @return array of strings
1312  */
1313 function quiz_question_list_instances($questionid) {
1314     global $CFG, $DB;
1316     // TODO MDL-5780: we should also consider other questions that are used by
1317     // random questions in this quiz, but that is very hard.
1319     $sql = "SELECT q.id, q.name
1320             FROM {quiz} q
1321             JOIN {quiz_question_instances} qqi ON q.id = qqi.quiz
1322             WHERE qqi.question = ?";
1324     if ($instances = $DB->get_records_sql_menu($sql, array($questionid))) {
1325         return $instances;
1326     }
1327     return array();
1330 /**
1331  * Implementation of the function for printing the form elements that control
1332  * whether the course reset functionality affects the quiz.
1333  *
1334  * @param $mform form passed by reference
1335  */
1336 function quiz_reset_course_form_definition(&$mform) {
1337     $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz'));
1338     $mform->addElement('advcheckbox', 'reset_quiz_attempts', get_string('removeallquizattempts','quiz'));
1341 /**
1342  * Course reset form defaults.
1343  * @return array
1344  */
1345 function quiz_reset_course_form_defaults($course) {
1346     return array('reset_quiz_attempts'=>1);
1349 /**
1350  * Removes all grades from gradebook
1351  *
1352  * @global stdClass
1353  * @global object
1354  * @param int $courseid
1355  * @param string optional type
1356  */
1357 function quiz_reset_gradebook($courseid, $type='') {
1358     global $CFG, $DB;
1360     $sql = "SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
1361               FROM {quiz} q, {course_modules} cm, {modules} m
1362              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=q.id AND q.course=?";
1364     if ($quizs = $DB->get_records_sql($sql, array($courseid))) {
1365         foreach ($quizs as $quiz) {
1366             quiz_grade_item_update($quiz, 'reset');
1367         }
1368     }
1371 /**
1372  * Actual implementation of the rest coures functionality, delete all the
1373  * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is
1374  * set and true.
1375  *
1376  * Also, move the quiz open and close dates, if the course start date is changing.
1377  *
1378  * @global stdClass
1379  * @global object
1380  * @param object $data the data submitted from the reset course.
1381  * @return array status array
1382  */
1383 function quiz_reset_userdata($data) {
1384     global $CFG, $DB;
1385     require_once($CFG->libdir.'/questionlib.php');
1387     $componentstr = get_string('modulenameplural', 'quiz');
1388     $status = array();
1390     /// Delete attempts.
1391     if (!empty($data->reset_quiz_attempts)) {
1392         $quizzes = $DB->get_records('quiz', array('course' => $data->courseid));
1393         foreach ($quizzes as $quiz) {
1394             quiz_delete_all_attempts($quiz);
1395         }
1397         // remove all grades from gradebook
1398         if (empty($data->reset_gradebook_grades)) {
1399             quiz_reset_gradebook($data->courseid);
1400         }
1401         $status[] = array('component' => $componentstr, 'item' => get_string('attemptsdeleted', 'quiz'), 'error' => false);
1402     }
1404     /// updating dates - shift may be negative too
1405     if ($data->timeshift) {
1406         shift_course_mod_dates('quiz', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
1407         $status[] = array('component' => $componentstr, 'item' => get_string('openclosedatesupdated', 'quiz'), 'error' => false);
1408     }
1410     return $status;
1413 /**
1414  * Checks whether the current user is allowed to view a file uploaded in a quiz.
1415  * Teachers can view any from their courses, students can only view their own.
1416  *
1417  * @global object
1418  * @global object
1419  * @uses CONTEXT_COURSE
1420  * @param int $attemptuniqueid int attempt id
1421  * @param int $questionid int question id
1422  * @return boolean to indicate access granted or denied
1423  */
1424 function quiz_check_file_access($attemptuniqueid, $questionid) {
1425     global $USER, $DB;
1427     $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $attemptuniqueid));
1428     $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz));
1429     $context = get_context_instance(CONTEXT_COURSE, $quiz->course);
1431     // access granted if the current user submitted this file
1432     if ($attempt->userid == $USER->id) {
1433         return true;
1434     // access granted if the current user has permission to grade quizzes in this course
1435     } else if (has_capability('mod/quiz:viewreports', $context) || has_capability('mod/quiz:grade', $context)) {
1436         return true;
1437     }
1439     // otherwise, this user does not have permission
1440     return false;
1443 /**
1444  * Prints quiz summaries on MyMoodle Page
1445  *
1446  * @global object
1447  * @global object
1448  * @param arry $courses
1449  * @param array $htmlarray
1450  */
1451 function quiz_print_overview($courses, &$htmlarray) {
1452     global $USER, $CFG;
1453 /// These next 6 Lines are constant in all modules (just change module name)
1454     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1455         return array();
1456     }
1458     if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
1459         return;
1460     }
1462 /// Fetch some language strings outside the main loop.
1463     $strquiz = get_string('modulename', 'quiz');
1464     $strnoattempts = get_string('noattempts', 'quiz');
1466 /// We want to list quizzes that are currently available, and which have a close date.
1467 /// This is the same as what the lesson does, and the dabate is in MDL-10568.
1468     $now = time();
1469     foreach ($quizzes as $quiz) {
1470         if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
1471         /// Give a link to the quiz, and the deadline.
1472             $str = '<div class="quiz overview">' .
1473                     '<div class="name">' . $strquiz . ': <a ' . ($quiz->visible ? '' : ' class="dimmed"') .
1474                     ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->coursemodule . '">' .
1475                     $quiz->name . '</a></div>';
1476             $str .= '<div class="info">' . get_string('quizcloseson', 'quiz', userdate($quiz->timeclose)) . '</div>';
1478         /// Now provide more information depending on the uers's role.
1479             $context = get_context_instance(CONTEXT_MODULE, $quiz->coursemodule);
1480             if (has_capability('mod/quiz:viewreports', $context)) {
1481             /// For teacher-like people, show a summary of the number of student attempts.
1482                 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
1483                 // fields set to make the following call work.
1484                 $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
1485             } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student
1486             /// For student-like people, tell them how many attempts they have made.
1487                 if (isset($USER->id) && ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
1488                     $numattempts = count($attempts);
1489                     $str .= '<div class="info">' . get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
1490                 } else {
1491                     $str .= '<div class="info">' . $strnoattempts . '</div>';
1492                 }
1493             } else {
1494             /// For ayone else, there is no point listing this quiz, so stop processing.
1495                 continue;
1496             }
1498         /// Add the output for this quiz to the rest.
1499             $str .= '</div>';
1500             if (empty($htmlarray[$quiz->course]['quiz'])) {
1501                 $htmlarray[$quiz->course]['quiz'] = $str;
1502             } else {
1503                 $htmlarray[$quiz->course]['quiz'] .= $str;
1504             }
1505         }
1506     }
1509 /**
1510  * Return a textual summary of the number of attemtps that have been made at a particular quiz,
1511  * returns '' if no attemtps have been made yet, unless $returnzero is passed as true.
1512  *
1513  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1514  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
1515  * @param boolean $returnzero if false (default), when no attempts have been made '' is returned instead of 'Attempts: 0'.
1516  * @param int $currentgroup if there is a concept of current group where this method is being called
1517  *         (e.g. a report) pass it in here. Default 0 which means no current group.
1518  * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
1519  *          "Attemtps 123 (45 from this group)".
1520  */
1521 function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
1522     global $DB, $USER;
1523     $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0));
1524     if ($numattempts || $returnzero) {
1525         if (groups_get_activity_groupmode($cm)) {
1526             $a->total = $numattempts;
1527             if ($currentgroup) {
1528                 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1529                         '{quiz_attempts} qa JOIN ' .
1530                         '{groups_members} gm ON qa.userid = gm.userid ' .
1531                         'WHERE quiz = ? AND preview = 0 AND groupid = ?', array($quiz->id, $currentgroup));
1532                 return get_string('attemptsnumthisgroup', 'quiz', $a);
1533             } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
1534                 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups));
1535                 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1536                         '{quiz_attempts} qa JOIN ' .
1537                         '{groups_members} gm ON qa.userid = gm.userid ' .
1538                         'WHERE quiz = ? AND preview = 0 AND ' .
1539                         "groupid $usql", array_merge(array($quiz->id), $params));
1540                 return get_string('attemptsnumyourgroups', 'quiz', $a);
1541             }
1542         }
1543         return get_string('attemptsnum', 'quiz', $numattempts);
1544     }
1545     return '';
1548 /**
1549  * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
1550  * to the quiz reports.
1551  *
1552  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1553  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
1554  * @param object $context the quiz context.
1555  * @param boolean $returnzero if false (default), when no attempts have been made '' is returned instead of 'Attempts: 0'.
1556  * @param int $currentgroup if there is a concept of current group where this method is being called
1557  *         (e.g. a report) pass it in here. Default 0 which means no current group.
1558  * @return string HTML fragment for the link.
1559  */
1560 function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false, $currentgroup = 0) {
1561     global $CFG;
1562     $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
1563     if (!$summary) {
1564         return '';
1565     }
1567     require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1568     $url = new moodle_url('/mod/quiz/report.php', array(
1569             'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
1570     return html_writer::link($url, $summary);
1573 /**
1574  * @param string $feature FEATURE_xx constant for requested feature
1575  * @return bool True if quiz supports feature
1576  */
1577 function quiz_supports($feature) {
1578     switch($feature) {
1579         case FEATURE_GROUPS:                  return true;
1580         case FEATURE_GROUPINGS:               return true;
1581         case FEATURE_GROUPMEMBERSONLY:        return true;
1582         case FEATURE_MOD_INTRO:               return true;
1583         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
1584         case FEATURE_GRADE_HAS_GRADE:         return true;
1585         case FEATURE_GRADE_OUTCOMES:          return true;
1587         default: return null;
1588     }
1591 /**
1592  * @global object
1593  * @global stdClass
1594  * @return array all other caps used in module
1595  */
1596 function quiz_get_extra_capabilities() {
1597     global $DB, $CFG;
1598     require_once($CFG->libdir.'/questionlib.php');
1599     $caps = question_get_all_capabilities();
1600     $reportcaps = $DB->get_records_select_menu('capabilities', 'name LIKE ?', array('quizreport/%'), 'id,name');
1601     $caps = array_merge($caps, $reportcaps);
1602     $caps[] = 'moodle/site:accessallgroups';
1603     return $caps;
1606 /**
1607  * This fucntion extends the global navigation for the site.
1608  * It is important to note that you should not rely on PAGE objects within this
1609  * body of code as there is no guarantee that during an AJAX request they are
1610  * available
1611  *
1612  * @param navigation_node $quiznode The quiz node within the global navigation
1613  * @param stdClass $course The course object returned from the DB
1614  * @param stdClass $module The module object returned from the DB
1615  * @param stdClass $cm The course module instance returned from the DB
1616  */
1617 function quiz_extend_navigation($quiznode, $course, $module, $cm) {
1618     global $CFG;
1620     $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1622     if (has_capability('mod/quiz:view', $context)) {
1623         $url = new moodle_url('/mod/quiz/view.php', array('id'=>$cm->id));
1624         $quiznode->add(get_string('info', 'quiz'), $url, navigation_node::TYPE_SETTING,
1625                 null, null, new pix_icon('i/info', ''));
1626     }
1628     if (has_capability('mod/quiz:viewreports', $context)) {
1629         require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php');
1630         $reportlist = quiz_report_list($context);
1632         $url = new moodle_url('/mod/quiz/report.php', array('id' => $cm->id, 'mode' => reset($reportlist)));
1633         $reportnode = $quiznode->add(get_string('results', 'quiz'), $url, navigation_node::TYPE_SETTING,
1634                 null, null, new pix_icon('i/report', ''));
1636         foreach ($reportlist as $report) {
1637             $url = new moodle_url('/mod/quiz/report.php', array('id' => $cm->id, 'mode' => $report));
1638             $reportnode->add(get_string($report, 'quiz_'.$report), $url, navigation_node::TYPE_SETTING,
1639                     null, 'quiz_report_' . $report, new pix_icon('i/item', ''));
1640         }
1641     }
1644 /**
1645  * This function extends the settings navigation block for the site.
1646  *
1647  * It is safe to rely on PAGE here as we will only ever be within the module
1648  * context when this is called
1649  *
1650  * @param settings_navigation $settings
1651  * @param navigation_node $quiznode
1652  */
1653 function quiz_extend_settings_navigation($settings, $quiznode) {
1654     global $PAGE, $CFG;
1656     /**
1657      * Require {@link questionlib.php}
1658      * Included here as we only ever want to include this file if we really need to.
1659      */
1660     require_once($CFG->libdir . '/questionlib.php');
1662     if (has_capability('mod/quiz:manageoverrides', $PAGE->cm->context)) {
1663         $url = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$PAGE->cm->id));
1664         $quiznode->add(get_string('groupoverrides', 'quiz'), new moodle_url($url, array('mode'=>'group')),
1665                 navigation_node::TYPE_SETTING, null, 'groupoverrides');
1666         $quiznode->add(get_string('useroverrides', 'quiz'), new moodle_url($url, array('mode'=>'user')),
1667                 navigation_node::TYPE_SETTING, null, 'useroverrides');
1668     }
1670     if (has_capability('mod/quiz:manage', $PAGE->cm->context)) {
1671         $url = new moodle_url('/mod/quiz/edit.php', array('cmid'=>$PAGE->cm->id));
1672         $text = get_string('editquiz', 'quiz');
1673         $quiznode->add($text, $url, navigation_node::TYPE_SETTING, null,
1674                 'mod_quiz_edit', new pix_icon('t/edit', ''));
1675     }
1677     if (has_capability('mod/quiz:preview', $PAGE->cm->context)) {
1678         $url = new moodle_url('/mod/quiz/startattempt.php', array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey()));
1679         $quiznode->add(get_string('preview', 'quiz'), $url, navigation_node::TYPE_SETTING,
1680                 null, 'mod_quiz_preview', new pix_icon('t/preview', ''));
1681     }
1683     question_extend_settings_navigation($quiznode, $PAGE->cm->context)->trim_if_empty();