38f799afd70a06b47286df0c56f31e573aa0ac5b
[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;
107     $cmid = $quiz->coursemodule;
109     // Process the options from the form.
110     $quiz->created = time();
111     $quiz->questions = '';
112     $result = quiz_process_options($quiz);
113     if ($result && is_string($result)) {
114         return $result;
115     }
117     // Try to store it in the database.
118     $quiz->id = $DB->insert_record('quiz', $quiz);
120     // Do the processing required after an add or an update.
121     quiz_after_add_or_update($quiz);
123     return $quiz->id;
126 /**
127  * Given an object containing all the necessary data,
128  * (defined by the form in mod_form.php) this function
129  * will update an existing instance with new data.
130  *
131  * @global stdClass
132  * @global object
133  * @param object $quiz the data that came from the form.
134  * @return mixed true on success, false or a string error message on failure.
135  */
136 function quiz_update_instance($quiz, $mform) {
137     global $CFG, $DB;
139     // Process the options from the form.
140     $result = quiz_process_options($quiz);
141     if ($result && is_string($result)) {
142         return $result;
143     }
145     // Repaginate, if asked to.
146     if (!$quiz->shufflequestions && !empty($quiz->repaginatenow)) {
147         require_once($CFG->dirroot . '/mod/quiz/locallib.php');
148         $quiz->questions = $DB->get_field('quiz', 'questions', array('id' => $quiz->instance));
149         $quiz->questions = quiz_repaginate($quiz->questions, $quiz->questionsperpage);
150     }
151     unset($quiz->repaginatenow);
153     // Update the database.
154     $quiz->id = $quiz->instance;
155     $DB->update_record('quiz', $quiz);
157     // Do the processing required after an add or an update.
158     quiz_after_add_or_update($quiz);
160     // Delete any previous preview attempts
161     quiz_delete_previews($quiz);
163     return true;
166 /**
167  * Given an ID of an instance of this module,
168  * this function will permanently delete the instance
169  * and any data that depends on it.
170  *
171  * @global object
172  * @param int $id
173  * @return bool
174  */
175 function quiz_delete_instance($id) {
176     global $DB;
178     if (!$quiz = $DB->get_record('quiz', array('id' => $id))) {
179         return false;
180     }
182     quiz_delete_all_attempts($quiz);
183     quiz_delete_all_overrides($quiz);
185     $DB->delete_records('quiz_question_instances', array('quiz' => $quiz->id));
186     $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
188     $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id));
189     foreach($events as $event) {
190         $event = calendar_event::load($event);
191         $event->delete();
192     }
194     quiz_grade_item_delete($quiz);
195     $DB->delete_records('quiz', array('id' => $quiz->id));
197     return true;
200 /**
201  * Deletes a quiz override from the database and clears any corresponding calendar events
202  *
203  * @param object $quiz The quiz object.
204  * @param integer $overrideid The id of the override being deleted
205  * @return bool true on success
206  */
207 function quiz_delete_override($quiz, $overrideid) {
208     global $DB;
210     if (!$override = $DB->get_record('quiz_overrides', array('id' => $overrideid))) {
211         return false;
212     }
213     $groupid   = empty($override->groupid)?   0 : $override->groupid;
214     $userid    = empty($override->userid)?    0 : $override->userid;
216     // Delete the events
217     $events = $DB->get_records('event', array('modulename'=>'quiz', 'instance'=>$quiz->id, 'groupid'=>$groupid, 'userid'=>$userid));
218     foreach($events as $event) {
219         $eventold = calendar_event::load($event);
220         $eventold->delete();
221     }
223     $DB->delete_records('quiz_overrides', array('id' => $overrideid));
224     return true;
227 /**
228  * Deletes all quiz overrides from the database and clears any corresponding calendar events
229  *
230  * @param object $quiz The quiz object.
231  */
232 function quiz_delete_all_overrides($quiz) {
233     global $DB;
235     $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id');
236     foreach ($overrides as $override) {
237         quiz_delete_override($quiz, $override->id);
238     }
241 /**
242  * Updates a quiz object with override information for a user.
243  *
244  * Algorithm:  For each quiz setting, if there is a matching user-specific override,
245  *   then use that otherwise, if there are group-specific overrides, return the most
246  *   lenient combination of them.  If neither applies, leave the quiz setting unchanged.
247  *
248  *   Special case: if there is more than one password that applies to the user, then
249  *   quiz->extrapasswords will contain an array of strings giving the remaining
250  *   passwords.
251  *
252  * @param object $quiz The quiz object.
253  * @param integer $userid The userid.
254  * @return object $quiz The updated quiz object.
255  */
256 function quiz_update_effective_access($quiz, $userid) {
257     global $DB;
259     // check for user override
260     $override = $DB->get_record('quiz_overrides', array('quiz' => $quiz->id, 'userid' => $userid));
262     if (!$override) {
263         $override = new stdClass();
264         $override->timeopen = null;
265         $override->timeclose = null;
266         $override->timelimit = null;
267         $override->attempts = null;
268         $override->password = null;
269     }
271     // check for group overrides
272     $groupings = groups_get_user_groups($quiz->course, $userid);
274     if (!empty($groupings[0])) {
275         // Select all overrides that apply to the User's groups
276         list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
277         $sql = "SELECT * FROM {quiz_overrides}
278                 WHERE groupid $extra AND quiz = ?";
279         $params[] = $quiz->id;
280         $records = $DB->get_records_sql($sql, $params);
282         // Combine the overrides
283         $opens = array();
284         $closes = array();
285         $limits = array();
286         $attempts = array();
287         $passwords = array();
289         foreach ($records as $gpoverride) {
290             if (isset($gpoverride->timeopen)) {
291                 $opens[] = $gpoverride->timeopen;
292             }
293             if (isset($gpoverride->timeclose)) {
294                 $closes[] = $gpoverride->timeclose;
295             }
296             if (isset($gpoverride->timelimit)) {
297                 $limits[] = $gpoverride->timelimit;
298             }
299             if (isset($gpoverride->attempts)) {
300                 $attempts[] = $gpoverride->attempts;
301             }
302             if (isset($gpoverride->password)) {
303                 $passwords[] = $gpoverride->password;
304             }
305         }
306         // If there is a user override for a setting, ignore the group override
307         if (is_null($override->timeopen) && count($opens)) {
308             $override->timeopen  = min($opens);
309         }
310         if (is_null($override->timeclose) && count($closes)) {
311             $override->timeclose  = max($closes);
312         }
313         if (is_null($override->timelimit) && count($limits)) {
314             $override->timelimit  = max($limits);
315         }
316         if (is_null($override->attempts) && count($attempts)) {
317             $override->attempts  = max($attempts);
318         }
319         if (is_null($override->password) && count($passwords)) {
320             $override->password  = array_shift($passwords);
321             if (count($passwords)) {
322                 $override->extrapasswords  = $passwords;
323             }
324         }
326     }
328     // merge with quiz defaults
329     $keys = array('timeopen','timeclose', 'timelimit', 'attempts', 'password', 'extrapasswords');
330     foreach ($keys as $key) {
331         if (isset($override->{$key})) {
332             $quiz->{$key} = $override->{$key};
333         }
334     }
336     return $quiz;
339 /**
340  * Delete all the attempts belonging to a quiz.
341  *
342  * @global stdClass
343  * @global object
344  * @param object $quiz The quiz object.
345  */
346 function quiz_delete_all_attempts($quiz) {
347     global $CFG, $DB;
348     require_once($CFG->libdir . '/questionlib.php');
349     $attempts = $DB->get_records('quiz_attempts', array('quiz' => $quiz->id));
350     foreach ($attempts as $attempt) {
351         delete_attempt($attempt->uniqueid);
352     }
353     $DB->delete_records('quiz_attempts', array('quiz' => $quiz->id));
354     $DB->delete_records('quiz_grades', array('quiz' => $quiz->id));
357 /**
358  * Return a small object with summary information about what a
359  * user has done with a given particular instance of this module
360  * Used for user activity reports.
361  * $return->time = the time they did it
362  * $return->info = a short text description
363  *
364  * @global object
365  * @param object $course
366  * @param object $user
367  * @param object $mod
368  * @param object $quiz
369  * @return object|null
370  */
371 function quiz_user_outline($course, $user, $mod, $quiz) {
372     global $DB, $CFG;
373     require_once("$CFG->libdir/gradelib.php");
374     $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
376     if (empty($grades->items[0]->grades)) {
377         return null;
378     } else {
379         $grade = reset($grades->items[0]->grades);
380     }
382     $result = new stdClass;
383     $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
384     $result->time = $grade->dategraded;
385     return $result;
388 /**
389  * Is this a graded quiz? If this method returns true, you can assume that
390  * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to
391  * divide by them).
392  *
393  * @param object $quiz a row from the quiz table.
394  * @return boolean whether this is a graded quiz.
395  */
396 function quiz_has_grades($quiz) {
397     return $quiz->grade != 0 && $quiz->sumgrades != 0;
400 /**
401  * Get the best current grade for a particular user in a quiz.
402  *
403  * @param object $quiz the quiz settings.
404  * @param integer $userid the id of the user.
405  * @return float the user's current grade for this quiz, or NULL if this user does
406  * not have a grade on this quiz.
407  */
408 function quiz_get_best_grade($quiz, $userid) {
409     global $DB;
410     $grade = $DB->get_field('quiz_grades', 'grade', array('quiz' => $quiz->id, 'userid' => $userid));
412     // Need to detect errors/no result, without catching 0 scores.
413     if ($grade === false) {
414         return null;
415     }
417     return $grade + 0; // Convert to number.
420 /**
421  * Print a detailed representation of what a  user has done with
422  * a given particular instance of this module, for user activity reports.
423  *
424  * @global object
425  * @param object $course
426  * @param object $user
427  * @param object $mod
428  * @param object $quiz
429  * @return bool
430  */
431 function quiz_user_complete($course, $user, $mod, $quiz) {
432     global $DB, $CFG, $OUTPUT;
433     require_once("$CFG->libdir/gradelib.php");
434     $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
435     if (!empty($grades->items[0]->grades)) {
436         $grade = reset($grades->items[0]->grades);
437         echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
438         if ($grade->str_feedback) {
439             echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
440         }
441     }
443     if ($attempts = $DB->get_records('quiz_attempts', array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) {
444         foreach ($attempts as $attempt) {
445             echo get_string('attempt', 'quiz').' '.$attempt->attempt.': ';
446             if ($attempt->timefinish == 0) {
447                 print_string('unfinished');
448             } else {
449                 echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' . quiz_format_grade($quiz, $quiz->sumgrades);
450             }
451             echo ' - '.userdate($attempt->timemodified).'<br />';
452         }
453     } else {
454        print_string('noattempts', 'quiz');
455     }
457     return true;
460 /**
461  * Function to be run periodically according to the moodle cron
462  * This function searches for things that need to be done, such
463  * as sending out mail, toggling flags etc ...
464  *
465  * @global stdClass
466  * @return bool true
467  */
468 function quiz_cron() {
469     global $CFG;
471     return true;
474 /**
475  * @global object
476  * @param integer $quizid the quiz id.
477  * @param integer $userid the userid.
478  * @param string $status 'all', 'finished' or 'unfinished' to control
479  * @param bool $includepreviews
480  * @return an array of all the user's attempts at this quiz. Returns an empty array if there are none.
481  */
482 function quiz_get_user_attempts($quizid, $userid=0, $status = 'finished', $includepreviews = false) {
483     global $DB;
484     $status_condition = array(
485         'all' => '',
486         'finished' => ' AND timefinish > 0',
487         'unfinished' => ' AND timefinish = 0'
488     );
489     $previewclause = '';
490     if (!$includepreviews) {
491         $previewclause = ' AND preview = 0';
492     }
493     $params=array($quizid);
494     if ($userid){
495         $userclause = ' AND userid = ?';
496         $params[]=$userid;
497     } else {
498         $userclause = '';
499     }
500     if ($attempts = $DB->get_records_select('quiz_attempts',
501             "quiz = ?" .$userclause. $previewclause . $status_condition[$status], $params,
502             'attempt ASC')) {
503         return $attempts;
504     } else {
505         return array();
506     }
509 /**
510  * Return grade for given user or all users.
511  *
512  * @global stdClass
513  * @global object
514  * @param int $quizid id of quiz
515  * @param int $userid optional user id, 0 means all users
516  * @return array array of grades, false if none. These are raw grades. They should
517  * be processed with quiz_format_grade for display.
518  */
519 function quiz_get_user_grades($quiz, $userid=0) {
520     global $CFG, $DB;
522     $params = array($quiz->id);
523     $wheresql = '';
524     if ($userid) {
525         $params[] = $userid;
526         $wheresql = "AND u.id = ?";
527     }
528     $sql = "SELECT u.id, u.id AS userid, g.grade AS rawgrade, g.timemodified AS dategraded, MAX(a.timefinish) AS datesubmitted
529             FROM {user} u, {quiz_grades} g, {quiz_attempts} a
530             WHERE u.id = g.userid AND g.quiz = ? AND a.quiz = g.quiz AND u.id = a.userid $wheresql
531             GROUP BY u.id, g.grade, g.timemodified";
533     return $DB->get_records_sql($sql, $params);
536 /**
537  * Round a grade to to the correct number of decimal places, and format it for display.
538  *
539  * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
540  * @param float $grade The grade to round.
541  * @return float
542  */
543 function quiz_format_grade($quiz, $grade) {
544     return format_float($grade, $quiz->decimalpoints);
547 /**
548  * Round a grade to to the correct number of decimal places, and format it for display.
549  *
550  * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
551  * @param float $grade The grade to round.
552  * @return float
553  */
554 function quiz_format_question_grade($quiz, $grade) {
555     if ($quiz->questiondecimalpoints == -1) {
556         return format_float($grade, $quiz->decimalpoints);
557     } else {
558         return format_float($grade, $quiz->questiondecimalpoints);
559     }
562 /**
563  * Update grades in central gradebook
564  *
565  * @global stdClass
566  * @global object
567  * @param object $quiz
568  * @param int $userid specific user only, 0 means all
569  */
570 function quiz_update_grades($quiz, $userid=0, $nullifnone=true) {
571     global $CFG, $DB;
572     require_once($CFG->libdir.'/gradelib.php');
574     if ($quiz->grade == 0) {
575         quiz_grade_item_update($quiz);
577     } else if ($grades = quiz_get_user_grades($quiz, $userid)) {
578         quiz_grade_item_update($quiz, $grades);
580     } else if ($userid and $nullifnone) {
581         $grade = new stdClass();
582         $grade->userid   = $userid;
583         $grade->rawgrade = NULL;
584         quiz_grade_item_update($quiz, $grade);
586     } else {
587         quiz_grade_item_update($quiz);
588     }
591 /**
592  * Update all grades in gradebook.
593  *
594  * @global object
595  */
596 function quiz_upgrade_grades() {
597     global $DB;
599     $sql = "SELECT COUNT('x')
600               FROM {quiz} a, {course_modules} cm, {modules} m
601              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
602     $count = $DB->count_records_sql($sql);
604     $sql = "SELECT a.*, cm.idnumber AS cmidnumber, a.course AS courseid
605               FROM {quiz} a, {course_modules} cm, {modules} m
606              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
607     if ($rs = $DB->get_recordset_sql($sql)) {
608         $pbar = new progress_bar('quizupgradegrades', 500, true);
609         $i=0;
610         foreach ($rs as $quiz) {
611             $i++;
612             upgrade_set_timeout(60*5); // set up timeout, may also abort execution
613             quiz_update_grades($quiz, 0, false);
614             $pbar->update($i, $count, "Updating Quiz grades ($i/$count).");
615         }
616         $rs->close();
617     }
620 /**
621  * Create grade item for given quiz
622  *
623  * @global stdClass
624  * @uses GRADE_TYPE_VALUE
625  * @uses GRADE_TYPE_NONE
626  * @uses QUIZ_REVIEW_SCORES
627  * @uses QUIZ_REVIEW_CLOSED
628  * @uses QUIZ_REVIEW_OPEN
629  * @uses PARAM_INT
630  * @uses GRADE_UPDATE_ITEM_LOCKED
631  * @param object $quiz object with extra cmidnumber
632  * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
633  * @return int 0 if ok, error code otherwise
634  */
635 function quiz_grade_item_update($quiz, $grades=NULL) {
636     global $CFG, $OUTPUT;
637     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
638         require_once($CFG->libdir.'/gradelib.php');
639     }
641     if (array_key_exists('cmidnumber', $quiz)) { //it may not be always present
642         $params = array('itemname'=>$quiz->name, 'idnumber'=>$quiz->cmidnumber);
643     } else {
644         $params = array('itemname'=>$quiz->name);
645     }
647     if ($quiz->grade > 0) {
648         $params['gradetype'] = GRADE_TYPE_VALUE;
649         $params['grademax']  = $quiz->grade;
650         $params['grademin']  = 0;
652     } else {
653         $params['gradetype'] = GRADE_TYPE_NONE;
654     }
656 /* description by TJ:
657 1/ If the quiz is set to not show scores while the quiz is still open, and is set to show scores after
658    the quiz is closed, then create the grade_item with a show-after date that is the quiz close date.
659 2/ If the quiz is set to not show scores at either of those times, create the grade_item as hidden.
660 3/ If the quiz is set to show scores, create the grade_item visible.
661 */
662     if (!($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
663     and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
664         $params['hidden'] = 1;
666     } else if ( ($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
667            and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
668         if ($quiz->timeclose) {
669             $params['hidden'] = $quiz->timeclose;
670         } else {
671             $params['hidden'] = 1;
672         }
674     } else {
675         // a) both open and closed enabled
676         // b) open enabled, closed disabled - we can not "hide after", grades are kept visible even after closing
677         $params['hidden'] = 0;
678     }
680     if ($grades  === 'reset') {
681         $params['reset'] = true;
682         $grades = NULL;
683     }
685     $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
686     if (!empty($gradebook_grades->items)) {
687         $grade_item = $gradebook_grades->items[0];
688         if ($grade_item->locked) {
689             $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
690             if (!$confirm_regrade) {
691                 $message = get_string('gradeitemislocked', 'grades');
692                 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id . '&amp;mode=overview';
693                 $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
694                 echo $OUTPUT->box_start('generalbox', 'notice');
695                 echo '<p>'. $message .'</p>';
696                 echo $OUTPUT->container_start('buttons');
697                 echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades'));
698                 echo $OUTPUT->single_button($back_link,  get_string('cancel'));
699                 echo $OUTPUT->container_end();
700                 echo $OUTPUT->box_end();
702                 return GRADE_UPDATE_ITEM_LOCKED;
703             }
704         }
705     }
707     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
710 /**
711  * Delete grade item for given quiz
712  *
713  * @global stdClass
714  * @param object $quiz object
715  * @return object quiz
716  */
717 function quiz_grade_item_delete($quiz) {
718     global $CFG;
719     require_once($CFG->libdir . '/gradelib.php');
721     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, NULL, array('deleted' => 1));
724 /**
725  * @return the options for calculating the quiz grade from the individual attempt grades.
726  */
727 function quiz_get_grading_options() {
728     return array (
729             QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'),
730             QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'),
731             QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'),
732             QUIZ_ATTEMPTLAST  => get_string('attemptlast', 'quiz'));
735 /**
736  * Returns an array of users who have data in a given quiz
737  *
738  * @global stdClass
739  * @global object
740  * @param int $quizid
741  * @return array
742  */
743 function quiz_get_participants($quizid) {
744     global $CFG, $DB;
746     //Get users from attempts
747     $us_attempts = $DB->get_records_sql("SELECT DISTINCT u.id, u.id
748                                     FROM {user} u,
749                                          {quiz_attempts} a
750                                     WHERE a.quiz = ? and
751                                           u.id = a.userid", array($quizid));
753     //Return us_attempts array (it contains an array of unique users)
754     return $us_attempts;
758 /**
759  * This standard function will check all instances of this module
760  * and make sure there are up-to-date events created for each of them.
761  * If courseid = 0, then every quiz event in the site is checked, else
762  * only quiz events belonging to the course specified are checked.
763  * This function is used, in its new format, by restore_refresh_events()
764  *
765  * @global object
766  * @uses QUIZ_MAX_EVENT_LENGTH
767  * @param int $courseid
768  * @return bool
769  */
770 function quiz_refresh_events($courseid = 0) {
771     global $DB;
773     if ($courseid == 0) {
774         if (! $quizzes = $DB->get_records('quiz')) {
775             return true;
776         }
777     } else {
778         if (! $quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
779             return true;
780         }
781     }
783     foreach ($quizzes as $quiz) {
784         quiz_update_events($quiz);
785     }
787     return true;
790 /**
791  * Returns all quiz graded users since a given time for specified quiz
792  */
793 function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
794         $courseid, $cmid, $userid = 0, $groupid = 0) {
795     global $CFG, $COURSE, $USER, $DB;
796     require_once('locallib.php');
798     if ($COURSE->id == $courseid) {
799         $course = $COURSE;
800     } else {
801         $course = $DB->get_record('course', array('id' => $courseid));
802     }
804     $modinfo =& get_fast_modinfo($course);
806     $cm = $modinfo->cms[$cmid];
807     $quiz = $DB->get_record('quiz', array('id' => $cm->instance));
809     if ($userid) {
810         $userselect = "AND u.id = :userid";
811         $params['userid'] = $userid;
812     } else {
813         $userselect = '';
814     }
816     if ($groupid) {
817         $groupselect = 'AND gm.groupid = :groupid';
818         $groupjoin   = 'JOIN {groups_members} gm ON  gm.userid=u.id';
819         $params['groupid'] = $groupid;
820     } else {
821         $groupselect = '';
822         $groupjoin   = '';
823     }
825     $params['timestart'] = $timestart;
826     $params['quizid'] = $quiz->id;
828     if (!$attempts = $DB->get_records_sql("
829               SELECT qa.*,
830                      u.firstname, u.lastname, u.email, u.picture, u.imagealt
831                 FROM {quiz_attempts} qa
832                      JOIN {user} u ON u.id = qa.userid
833                      $groupjoin
834                WHERE qa.timefinish > :timestart
835                  AND qa.quiz = :quizid
836                  AND qa.preview = 0
837                      $userselect
838                      $groupselect
839             ORDER BY qa.timefinish ASC", $params)) {
840         return;
841     }
843     $context         = get_context_instance(CONTEXT_MODULE, $cm->id);
844     $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
845     $viewfullnames   = has_capability('moodle/site:viewfullnames', $context);
846     $grader          = has_capability('mod/quiz:viewreports', $context);
847     $groupmode       = groups_get_activity_groupmode($cm, $course);
849     if (is_null($modinfo->groups)) {
850         $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
851     }
853     $usersgroups = null;
854     $aname = format_string($cm->name,true);
855     foreach ($attempts as $attempt) {
856         if ($attempt->userid != $USER->id) {
857             if (!$grader) {
858                 // Grade permission required
859                 continue;
860             }
862             if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
863                 if (is_null($usersgroups)) {
864                     $usersgroups = groups_get_all_groups($course->id,
865                             $attempt->userid, $cm->groupingid);
866                     if (is_array($usersgroups)) {
867                         $usersgroups = array_keys($usersgroups);
868                     } else {
869                         $usersgroups = array();
870                     }
871                 }
872                 if (!array_intersect($usersgroups, $modinfo->groups[$cm->id])) {
873                     continue;
874                 }
875             }
876         }
878         $options = quiz_get_reviewoptions($quiz, $attempt, $context);
880         $tmpactivity = new stdClass;
882         $tmpactivity->type       = 'quiz';
883         $tmpactivity->cmid       = $cm->id;
884         $tmpactivity->name       = $aname;
885         $tmpactivity->sectionnum = $cm->sectionnum;
886         $tmpactivity->timestamp  = $attempt->timefinish;
888         $tmpactivity->content->attemptid = $attempt->id;
889         $tmpactivity->content->attempt   = $attempt->attempt;
890         if (quiz_has_grades($quiz) && $options->scores) {
891             $tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades);
892             $tmpactivity->content->maxgrade  = quiz_format_grade($quiz, $quiz->sumgrades);
893         } else {
894             $tmpactivity->content->sumgrades = null;
895             $tmpactivity->content->maxgrade  = null;
896         }
898         $tmpactivity->user->id        = $attempt->userid;
899         $tmpactivity->user->firstname = $attempt->firstname;
900         $tmpactivity->user->lastname = $attempt->lastname;
901         $tmpactivity->user->fullname = fullname($attempt, $viewfullnames);
902         $tmpactivity->user->picture  = $attempt->picture;
903         $tmpactivity->user->imagealt = $attempt->imagealt;
904         $tmpactivity->user->email = $attempt->email;
906         $activities[$index++] = $tmpactivity;
907     }
909   return;
912 function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
913     global $CFG, $OUTPUT;
915     echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">';
917     echo '<tr><td class="userpicture" valign="top">';
918     echo $OUTPUT->user_picture($activity->user, array('courseid' => $courseid));
919     echo '</td><td>';
921     if ($detail) {
922         $modname = $modnames[$activity->type];
923         echo '<div class="title">';
924         echo '<img src="' . $OUTPUT->pix_url('icon', $activity->type) . '" ' .
925                 'class="icon" alt="' . $modname . '" />';
926         echo '<a href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
927                 $activity->cmid . '">' . $activity->name . '</a>';
928         echo '</div>';
929     }
931     echo '<div class="grade">';
932     echo  get_string('attempt', 'quiz', $activity->content->attempt);
933     if (isset($activity->content->maxgrade)) {
934         $grades = $activity->content->sumgrades . ' / ' . $activity->content->maxgrade;
935         echo ': (<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?attempt=' .
936                 $activity->content->attemptid . '">' . $grades . '</a>)';
937     }
938     echo '</div>';
940     echo '<div class="user">';
941     echo '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $activity->user->id .
942             '&amp;course=' . $courseid . '">' . $activity->user->fullname .
943             '</a> - ' . userdate($activity->timestamp);
944     echo '</div>';
946     echo '</td></tr></table>';
948     return;
951 /**
952  * Pre-process the quiz options form data, making any necessary adjustments.
953  * Called by add/update instance in this file.
954  *
955  * @uses QUIZ_REVIEW_OVERALLFEEDBACK
956  * @uses QUIZ_REVIEW_CLOSED
957  * @uses QUIZ_REVIEW_OPEN
958  * @uses QUIZ_REVIEW_IMMEDIATELY
959  * @uses QUIZ_REVIEW_GENERALFEEDBACK
960  * @uses QUIZ_REVIEW_SOLUTIONS
961  * @uses QUIZ_REVIEW_ANSWERS
962  * @uses QUIZ_REVIEW_FEEDBACK
963  * @uses QUIZ_REVIEW_SCORES
964  * @uses QUIZ_REVIEW_RESPONSES
965  * @uses QUESTION_ADAPTIVE
966  * @param object $quiz The variables set on the form.
967  * @return string
968  */
969 function quiz_process_options(&$quiz) {
970     $quiz->timemodified = time();
972     // Quiz name.
973     if (!empty($quiz->name)) {
974         $quiz->name = trim($quiz->name);
975     }
977     // Password field - different in form to stop browsers that remember passwords
978     // getting confused.
979     $quiz->password = $quiz->quizpassword;
980     unset($quiz->quizpassword);
982     // Quiz feedback
983     if (isset($quiz->feedbacktext)) {
984         // Clean up the boundary text.
985         for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
986             if (empty($quiz->feedbacktext[$i]['text'])) {
987                 $quiz->feedbacktext[$i]['text'] = '';
988             } else {
989                 $quiz->feedbacktext[$i]['text'] = trim($quiz->feedbacktext[$i]['text']);
990             }
991         }
993         // Check the boundary value is a number or a percentage, and in range.
994         $i = 0;
995         while (!empty($quiz->feedbackboundaries[$i])) {
996             $boundary = trim($quiz->feedbackboundaries[$i]);
997             if (!is_numeric($boundary)) {
998                 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
999                     $boundary = trim(substr($boundary, 0, -1));
1000                     if (is_numeric($boundary)) {
1001                         $boundary = $boundary * $quiz->grade / 100.0;
1002                     } else {
1003                         return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
1004                     }
1005                 }
1006             }
1007             if ($boundary <= 0 || $boundary >= $quiz->grade) {
1008                 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
1009             }
1010             if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
1011                 return get_string('feedbackerrororder', 'quiz', $i + 1);
1012             }
1013             $quiz->feedbackboundaries[$i] = $boundary;
1014             $i += 1;
1015         }
1016         $numboundaries = $i;
1018         // Check there is nothing in the remaining unused fields.
1019         if (!empty($quiz->feedbackboundaries)) {
1020             for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
1021                 if (!empty($quiz->feedbackboundaries[$i]) && trim($quiz->feedbackboundaries[$i]) != '') {
1022                     return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
1023                 }
1024             }
1025         }
1026         for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
1027             if (!empty($quiz->feedbacktext[$i]['text']) && trim($quiz->feedbacktext[$i]['text']) != '') {
1028                 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
1029             }
1030         }
1031         $quiz->feedbackboundaries[-1] = $quiz->grade + 1; // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
1032         $quiz->feedbackboundaries[$numboundaries] = 0;
1033         $quiz->feedbackboundarycount = $numboundaries;
1034     }
1036     // Settings that get combined to go into the optionflags column.
1037     $quiz->optionflags = 0;
1038     if (!empty($quiz->adaptive)) {
1039         $quiz->optionflags |= QUESTION_ADAPTIVE;
1040     }
1042     // Settings that get combined to go into the review column.
1043     $review = 0;
1044     if (isset($quiz->responsesimmediately)) {
1045         $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY);
1046         unset($quiz->responsesimmediately);
1047     }
1048     if (isset($quiz->responsesopen)) {
1049         $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN);
1050         unset($quiz->responsesopen);
1051     }
1052     if (isset($quiz->responsesclosed)) {
1053         $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED);
1054         unset($quiz->responsesclosed);
1055     }
1057     if (isset($quiz->scoreimmediately)) {
1058         $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY);
1059         unset($quiz->scoreimmediately);
1060     }
1061     if (isset($quiz->scoreopen)) {
1062         $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN);
1063         unset($quiz->scoreopen);
1064     }
1065     if (isset($quiz->scoreclosed)) {
1066         $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED);
1067         unset($quiz->scoreclosed);
1068     }
1070     if (isset($quiz->feedbackimmediately)) {
1071         $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1072         unset($quiz->feedbackimmediately);
1073     }
1074     if (isset($quiz->feedbackopen)) {
1075         $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN);
1076         unset($quiz->feedbackopen);
1077     }
1078     if (isset($quiz->feedbackclosed)) {
1079         $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED);
1080         unset($quiz->feedbackclosed);
1081     }
1083     if (isset($quiz->answersimmediately)) {
1084         $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
1085         unset($quiz->answersimmediately);
1086     }
1087     if (isset($quiz->answersopen)) {
1088         $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN);
1089         unset($quiz->answersopen);
1090     }
1091     if (isset($quiz->answersclosed)) {
1092         $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED);
1093         unset($quiz->answersclosed);
1094     }
1096     if (isset($quiz->solutionsimmediately)) {
1097         $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_IMMEDIATELY);
1098         unset($quiz->solutionsimmediately);
1099     }
1100     if (isset($quiz->solutionsopen)) {
1101         $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_OPEN);
1102         unset($quiz->solutionsopen);
1103     }
1104     if (isset($quiz->solutionsclosed)) {
1105         $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_CLOSED);
1106         unset($quiz->solutionsclosed);
1107     }
1109     if (isset($quiz->generalfeedbackimmediately)) {
1110         $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1111         unset($quiz->generalfeedbackimmediately);
1112     }
1113     if (isset($quiz->generalfeedbackopen)) {
1114         $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_OPEN);
1115         unset($quiz->generalfeedbackopen);
1116     }
1117     if (isset($quiz->generalfeedbackclosed)) {
1118         $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_CLOSED);
1119         unset($quiz->generalfeedbackclosed);
1120     }
1122     if (isset($quiz->overallfeedbackimmediately)) {
1123         $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1124         unset($quiz->overallfeedbackimmediately);
1125     }
1126     if (isset($quiz->overallfeedbackopen)) {
1127         $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_OPEN);
1128         unset($quiz->overallfeedbackopen);
1129     }
1130     if (isset($quiz->overallfeedbackclosed)) {
1131         $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_CLOSED);
1132         unset($quiz->overallfeedbackclosed);
1133     }
1135     $quiz->review = $review;
1138 /**
1139  * This function is called at the end of quiz_add_instance
1140  * and quiz_update_instance, to do the common processing.
1141  *
1142  * @global object
1143  * @uses QUIZ_MAX_EVENT_LENGTH
1144  * @param object $quiz the quiz object.
1145  * @return void|string Void or error message
1146  */
1147 function quiz_after_add_or_update($quiz) {
1148     global $DB;
1149     $cmid = $quiz->coursemodule;
1151     // we need to use context now, so we need to make sure all needed info is already in db
1152     $DB->set_field('course_modules', 'instance', $quiz->id, array('id'=>$cmid));
1153     $context = get_context_instance(CONTEXT_MODULE, $cmid);
1155     // Save the feedback
1156     $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
1158     for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) {
1159         $feedback = new stdClass;
1160         $feedback->quizid = $quiz->id;
1161         $feedback->feedbacktext = $quiz->feedbacktext[$i]['text'];
1162         $feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format'];
1163         $feedback->mingrade = $quiz->feedbackboundaries[$i];
1164         $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
1165         $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1166         $feedbacktext = file_save_draft_area_files((int)$quiz->feedbacktext[$i]['itemid'], $context->id, 'mod_quiz', 'feedback', $feedback->id, array('subdirs'=>false, 'maxfiles'=>-1, 'maxbytes'=>0), $quiz->feedbacktext[$i]['text']);
1167         $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext, array('id'=>$feedback->id));
1168     }
1170     // Update the events relating to this quiz.
1171     quiz_update_events($quiz);
1173     //update related grade item
1174     quiz_grade_item_update($quiz);
1178 /**
1179  * This function updates the events associated to the quiz.
1180  * If $override is non-zero, then it updates only the events
1181  * associated with the specified override.
1182  *
1183  * @uses QUIZ_MAX_EVENT_LENGTH
1184  * @param object $quiz the quiz object.
1185  * @param object optional $override limit to a specific override
1186  */
1187 function quiz_update_events($quiz, $override = null) {
1188     global $DB;
1190     // Load the old events relating to this quiz.
1191     $conds = array('modulename'=>'quiz',
1192                    'instance'=>$quiz->id);
1193     if (!empty($override)) {
1194         // only load events for this override
1195         $conds['groupid'] = isset($override->groupid)?  $override->groupid : 0;
1196         $conds['userid'] = isset($override->userid)?  $override->userid : 0;
1197     }
1198     $oldevents = $DB->get_records('event', $conds);
1200     // Now make a todo list of all that needs to be updated
1201     if (empty($override)) {
1202         // We are updating the primary settings for the quiz, so we
1203         // need to add all the overrides
1204         $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id));
1205         // as well as the original quiz (empty override)
1206         $overrides[] = new stdClass;
1207     }
1208     else {
1209         // Just do the one override
1210         $overrides = array($override);
1211     }
1213     foreach ($overrides as $current) {
1214         $groupid   = isset($current->groupid)?  $current->groupid : 0;
1215         $userid    = isset($current->userid)? $current->userid : 0;
1216         $timeopen  = isset($current->timeopen)?  $current->timeopen : $quiz->timeopen;
1217         $timeclose = isset($current->timeclose)? $current->timeclose : $quiz->timeclose;
1219         // only add open/close events for an override if they differ from the quiz default
1220         $addopen  = empty($current->id) || !empty($current->timeopen);
1221         $addclose = empty($current->id) || !empty($current->timeclose);
1223         $event = new stdClass;
1224         $event->description = $quiz->intro;
1225         $event->courseid    = ($userid) ? 0 : $quiz->course; // Events module won't show user events when the courseid is nonzero
1226         $event->groupid     = $groupid;
1227         $event->userid      = $userid;
1228         $event->modulename  = 'quiz';
1229         $event->instance    = $quiz->id;
1230         $event->timestart   = $timeopen;
1231         $event->timeduration = max($timeclose - $timeopen, 0);
1232         $event->visible     = instance_is_visible('quiz', $quiz);
1233         $event->eventtype   = 'open';
1235         // Determine the event name
1236         if ($groupid) {
1237             $params = new stdClass;
1238             $params->quiz = $quiz->name;
1239             $params->group = groups_get_group_name($groupid);
1240             if ($params->group === false) {
1241                 // group doesn't exist, just skip it
1242                 continue;
1243             }
1244             $eventname = get_string('overridegroupeventname', 'quiz', $params);
1245         }
1246         else if ($userid) {
1247             $params = new stdClass;
1248             $params->quiz = $quiz->name;
1249             $eventname = get_string('overrideusereventname', 'quiz', $params);
1250         } else {
1251             $eventname = $quiz->name;
1252         }
1253         if ($addopen or $addclose) {
1254             if ($timeclose and $timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) {
1255                 // Single event for the whole quiz.
1256                 if ($oldevent = array_shift($oldevents)) {
1257                     $event->id = $oldevent->id;
1258                 }
1259                 else {
1260                     unset($event->id);
1261                 }
1262                 $event->name = $eventname;
1263                 // calendar_event::create will reuse a db record if the id field is set
1264                 calendar_event::create($event);
1265             } else {
1266                 // Separate start and end events.
1267                 $event->timeduration  = 0;
1268                 if ($timeopen && $addopen) {
1269                     if ($oldevent = array_shift($oldevents)) {
1270                         $event->id = $oldevent->id;
1271                     }
1272                     else {
1273                         unset($event->id);
1274                     }
1275                     $event->name = $eventname.' ('.get_string('quizopens', 'quiz').')';
1276                     // calendar_event::create will reuse a db record if the id field is set
1277                     calendar_event::create($event);
1278                 }
1279                 if ($timeclose && $addclose) {
1280                     if ($oldevent = array_shift($oldevents)) {
1281                         $event->id = $oldevent->id;
1282                     }
1283                     else {
1284                         unset($event->id);
1285                     }
1286                     $event->name      = $eventname.' ('.get_string('quizcloses', 'quiz').')';
1287                     $event->timestart = $timeclose;
1288                     $event->eventtype = 'close';
1289                     calendar_event::create($event);
1290                 }
1291             }
1292         }
1293     }
1295     // Delete any leftover events
1296     foreach ($oldevents as $badevent) {
1297         $badevent = calendar_event::load($badevent);
1298         $badevent->delete();
1299     }
1302 /**
1303  * @return array
1304  */
1305 function quiz_get_view_actions() {
1306     return array('view', 'view all', 'report', 'review');
1309 /**
1310  * @return array
1311  */
1312 function quiz_get_post_actions() {
1313     return array('attempt', 'close attempt', 'preview', 'editquestions', 'delete attempt', 'manualgrade');
1316 /**
1317  * Returns an array of names of quizzes that use this question
1318  *
1319  * @param integer $questionid
1320  * @return array of strings
1321  */
1322 function quiz_question_list_instances($questionid) {
1323     global $CFG, $DB;
1325     // TODO MDL-5780: we should also consider other questions that are used by
1326     // random questions in this quiz, but that is very hard.
1328     $sql = "SELECT q.id, q.name
1329             FROM {quiz} q
1330             JOIN {quiz_question_instances} qqi ON q.id = qqi.quiz
1331             WHERE qqi.question = ?";
1333     if ($instances = $DB->get_records_sql_menu($sql, array($questionid))) {
1334         return $instances;
1335     }
1336     return array();
1339 /**
1340  * Implementation of the function for printing the form elements that control
1341  * whether the course reset functionality affects the quiz.
1342  *
1343  * @param $mform form passed by reference
1344  */
1345 function quiz_reset_course_form_definition(&$mform) {
1346     $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz'));
1347     $mform->addElement('advcheckbox', 'reset_quiz_attempts', get_string('removeallquizattempts','quiz'));
1350 /**
1351  * Course reset form defaults.
1352  * @return array
1353  */
1354 function quiz_reset_course_form_defaults($course) {
1355     return array('reset_quiz_attempts'=>1);
1358 /**
1359  * Removes all grades from gradebook
1360  *
1361  * @global stdClass
1362  * @global object
1363  * @param int $courseid
1364  * @param string optional type
1365  */
1366 function quiz_reset_gradebook($courseid, $type='') {
1367     global $CFG, $DB;
1369     $sql = "SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
1370               FROM {quiz} q, {course_modules} cm, {modules} m
1371              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=q.id AND q.course=?";
1373     if ($quizs = $DB->get_records_sql($sql, array($courseid))) {
1374         foreach ($quizs as $quiz) {
1375             quiz_grade_item_update($quiz, 'reset');
1376         }
1377     }
1380 /**
1381  * Actual implementation of the reset course functionality, delete all the
1382  * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is
1383  * set and true.
1384  *
1385  * Also, move the quiz open and close dates, if the course start date is changing.
1386  *
1387  * @global stdClass
1388  * @global object
1389  * @param object $data the data submitted from the reset course.
1390  * @return array status array
1391  */
1392 function quiz_reset_userdata($data) {
1393     global $CFG, $DB;
1394     require_once($CFG->libdir.'/questionlib.php');
1396     $componentstr = get_string('modulenameplural', 'quiz');
1397     $status = array();
1399     /// Delete attempts.
1400     if (!empty($data->reset_quiz_attempts)) {
1401         $quizzes = $DB->get_records('quiz', array('course' => $data->courseid));
1402         foreach ($quizzes as $quiz) {
1403             quiz_delete_all_attempts($quiz);
1404         }
1406         // remove all grades from gradebook
1407         if (empty($data->reset_gradebook_grades)) {
1408             quiz_reset_gradebook($data->courseid);
1409         }
1410         $status[] = array('component' => $componentstr, 'item' => get_string('attemptsdeleted', 'quiz'), 'error' => false);
1411     }
1413     /// updating dates - shift may be negative too
1414     if ($data->timeshift) {
1415         shift_course_mod_dates('quiz', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
1416         $status[] = array('component' => $componentstr, 'item' => get_string('openclosedatesupdated', 'quiz'), 'error' => false);
1417     }
1419     return $status;
1422 /**
1423  * Checks whether the current user is allowed to view a file uploaded in a quiz.
1424  * Teachers can view any from their courses, students can only view their own.
1425  *
1426  * @global object
1427  * @global object
1428  * @uses CONTEXT_COURSE
1429  * @param int $attemptuniqueid int attempt id
1430  * @param int $questionid int question id
1431  * @return boolean to indicate access granted or denied
1432  */
1433 function quiz_check_file_access($attemptuniqueid, $questionid, $context = null) {
1434     global $USER, $DB, $CFG;
1435     require_once(dirname(__FILE__).'/attemptlib.php');
1436     require_once(dirname(__FILE__).'/locallib.php');
1438     $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $attemptuniqueid));
1439     $attemptobj = quiz_attempt::create($attempt->id);
1441     // does question exist?
1442     if (!$question = $DB->get_record('question', array('id' => $questionid))) {
1443         return false;
1444     }
1446     if ($context === null) {
1447         $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz));
1448         $cm = get_coursemodule_from_id('quiz', $quiz->id);
1449         $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1450     }
1452     // Load those questions and the associated states.
1453     $attemptobj->load_questions(array($questionid));
1454     $attemptobj->load_question_states(array($questionid));
1456     // obtain state
1457     $state = $attemptobj->get_question_state($questionid);
1458     // obtain questoin
1459     $question = $attemptobj->get_question($questionid);
1461     // access granted if the current user submitted this file
1462     if ($attempt->userid != $USER->id) {
1463         return false;
1464     // access granted if the current user has permission to grade quizzes in this course
1465     }
1466     if (!(has_capability('mod/quiz:viewreports', $context) || has_capability('mod/quiz:grade', $context))) {
1467         return false;
1468     }
1470     return array($question, $state, array());
1473 /**
1474  * Prints quiz summaries on MyMoodle Page
1475  *
1476  * @global object
1477  * @global object
1478  * @param arry $courses
1479  * @param array $htmlarray
1480  */
1481 function quiz_print_overview($courses, &$htmlarray) {
1482     global $USER, $CFG;
1483 /// These next 6 Lines are constant in all modules (just change module name)
1484     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1485         return array();
1486     }
1488     if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
1489         return;
1490     }
1492 /// Fetch some language strings outside the main loop.
1493     $strquiz = get_string('modulename', 'quiz');
1494     $strnoattempts = get_string('noattempts', 'quiz');
1496 /// We want to list quizzes that are currently available, and which have a close date.
1497 /// This is the same as what the lesson does, and the dabate is in MDL-10568.
1498     $now = time();
1499     foreach ($quizzes as $quiz) {
1500         if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
1501         /// Give a link to the quiz, and the deadline.
1502             $str = '<div class="quiz overview">' .
1503                     '<div class="name">' . $strquiz . ': <a ' . ($quiz->visible ? '' : ' class="dimmed"') .
1504                     ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->coursemodule . '">' .
1505                     $quiz->name . '</a></div>';
1506             $str .= '<div class="info">' . get_string('quizcloseson', 'quiz', userdate($quiz->timeclose)) . '</div>';
1508         /// Now provide more information depending on the uers's role.
1509             $context = get_context_instance(CONTEXT_MODULE, $quiz->coursemodule);
1510             if (has_capability('mod/quiz:viewreports', $context)) {
1511             /// For teacher-like people, show a summary of the number of student attempts.
1512                 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
1513                 // fields set to make the following call work.
1514                 $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
1515             } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student
1516             /// For student-like people, tell them how many attempts they have made.
1517                 if (isset($USER->id) && ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
1518                     $numattempts = count($attempts);
1519                     $str .= '<div class="info">' . get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
1520                 } else {
1521                     $str .= '<div class="info">' . $strnoattempts . '</div>';
1522                 }
1523             } else {
1524             /// For ayone else, there is no point listing this quiz, so stop processing.
1525                 continue;
1526             }
1528         /// Add the output for this quiz to the rest.
1529             $str .= '</div>';
1530             if (empty($htmlarray[$quiz->course]['quiz'])) {
1531                 $htmlarray[$quiz->course]['quiz'] = $str;
1532             } else {
1533                 $htmlarray[$quiz->course]['quiz'] .= $str;
1534             }
1535         }
1536     }
1539 /**
1540  * Return a textual summary of the number of attemtps that have been made at a particular quiz,
1541  * returns '' if no attemtps have been made yet, unless $returnzero is passed as true.
1542  *
1543  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1544  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
1545  * @param boolean $returnzero if false (default), when no attempts have been made '' is returned instead of 'Attempts: 0'.
1546  * @param int $currentgroup if there is a concept of current group where this method is being called
1547  *         (e.g. a report) pass it in here. Default 0 which means no current group.
1548  * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
1549  *          "Attemtps 123 (45 from this group)".
1550  */
1551 function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
1552     global $DB, $USER;
1553     $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0));
1554     if ($numattempts || $returnzero) {
1555         if (groups_get_activity_groupmode($cm)) {
1556             $a->total = $numattempts;
1557             if ($currentgroup) {
1558                 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1559                         '{quiz_attempts} qa JOIN ' .
1560                         '{groups_members} gm ON qa.userid = gm.userid ' .
1561                         'WHERE quiz = ? AND preview = 0 AND groupid = ?', array($quiz->id, $currentgroup));
1562                 return get_string('attemptsnumthisgroup', 'quiz', $a);
1563             } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
1564                 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups));
1565                 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1566                         '{quiz_attempts} qa JOIN ' .
1567                         '{groups_members} gm ON qa.userid = gm.userid ' .
1568                         'WHERE quiz = ? AND preview = 0 AND ' .
1569                         "groupid $usql", array_merge(array($quiz->id), $params));
1570                 return get_string('attemptsnumyourgroups', 'quiz', $a);
1571             }
1572         }
1573         return get_string('attemptsnum', 'quiz', $numattempts);
1574     }
1575     return '';
1578 /**
1579  * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
1580  * to the quiz reports.
1581  *
1582  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1583  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
1584  * @param object $context the quiz context.
1585  * @param boolean $returnzero if false (default), when no attempts have been made '' is returned instead of 'Attempts: 0'.
1586  * @param int $currentgroup if there is a concept of current group where this method is being called
1587  *         (e.g. a report) pass it in here. Default 0 which means no current group.
1588  * @return string HTML fragment for the link.
1589  */
1590 function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false, $currentgroup = 0) {
1591     global $CFG;
1592     $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
1593     if (!$summary) {
1594         return '';
1595     }
1597     require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1598     $url = new moodle_url('/mod/quiz/report.php', array(
1599             'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
1600     return html_writer::link($url, $summary);
1603 /**
1604  * @param string $feature FEATURE_xx constant for requested feature
1605  * @return bool True if quiz supports feature
1606  */
1607 function quiz_supports($feature) {
1608     switch($feature) {
1609         case FEATURE_GROUPS:                  return true;
1610         case FEATURE_GROUPINGS:               return true;
1611         case FEATURE_GROUPMEMBERSONLY:        return true;
1612         case FEATURE_MOD_INTRO:               return true;
1613         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
1614         case FEATURE_GRADE_HAS_GRADE:         return true;
1615         case FEATURE_GRADE_OUTCOMES:          return true;
1616         case FEATURE_BACKUP_MOODLE2:          return true;
1618         default: return null;
1619     }
1622 /**
1623  * @global object
1624  * @global stdClass
1625  * @return array all other caps used in module
1626  */
1627 function quiz_get_extra_capabilities() {
1628     global $DB, $CFG;
1629     require_once($CFG->libdir.'/questionlib.php');
1630     $caps = question_get_all_capabilities();
1631     $reportcaps = $DB->get_records_select_menu('capabilities', 'name LIKE ?', array('quizreport/%'), 'id,name');
1632     $caps = array_merge($caps, $reportcaps);
1633     $caps[] = 'moodle/site:accessallgroups';
1634     return $caps;
1637 /**
1638  * This fucntion extends the global navigation for the site.
1639  * It is important to note that you should not rely on PAGE objects within this
1640  * body of code as there is no guarantee that during an AJAX request they are
1641  * available
1642  *
1643  * @param navigation_node $quiznode The quiz node within the global navigation
1644  * @param stdClass $course The course object returned from the DB
1645  * @param stdClass $module The module object returned from the DB
1646  * @param stdClass $cm The course module instance returned from the DB
1647  */
1648 function quiz_extend_navigation($quiznode, $course, $module, $cm) {
1649     global $CFG;
1651     $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1653     if (has_capability('mod/quiz:view', $context)) {
1654         $url = new moodle_url('/mod/quiz/view.php', array('id'=>$cm->id));
1655         $quiznode->add(get_string('info', 'quiz'), $url, navigation_node::TYPE_SETTING,
1656                 null, null, new pix_icon('i/info', ''));
1657     }
1659     if (has_capability('mod/quiz:viewreports', $context)) {
1660         require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php');
1661         $reportlist = quiz_report_list($context);
1663         $url = new moodle_url('/mod/quiz/report.php', array('id' => $cm->id, 'mode' => reset($reportlist)));
1664         $reportnode = $quiznode->add(get_string('results', 'quiz'), $url, navigation_node::TYPE_SETTING,
1665                 null, null, new pix_icon('i/report', ''));
1667         foreach ($reportlist as $report) {
1668             $url = new moodle_url('/mod/quiz/report.php', array('id' => $cm->id, 'mode' => $report));
1669             $reportnode->add(get_string($report, 'quiz_'.$report), $url, navigation_node::TYPE_SETTING,
1670                     null, 'quiz_report_' . $report, new pix_icon('i/item', ''));
1671         }
1672     }
1675 /**
1676  * This function extends the settings navigation block for the site.
1677  *
1678  * It is safe to rely on PAGE here as we will only ever be within the module
1679  * context when this is called
1680  *
1681  * @param settings_navigation $settings
1682  * @param navigation_node $quiznode
1683  */
1684 function quiz_extend_settings_navigation($settings, $quiznode) {
1685     global $PAGE, $CFG;
1687     /**
1688      * Require {@link questionlib.php}
1689      * Included here as we only ever want to include this file if we really need to.
1690      */
1691     require_once($CFG->libdir . '/questionlib.php');
1693     if (has_capability('mod/quiz:manageoverrides', $PAGE->cm->context)) {
1694         $url = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$PAGE->cm->id));
1695         $quiznode->add(get_string('groupoverrides', 'quiz'), new moodle_url($url, array('mode'=>'group')),
1696                 navigation_node::TYPE_SETTING, null, 'groupoverrides');
1697         $quiznode->add(get_string('useroverrides', 'quiz'), new moodle_url($url, array('mode'=>'user')),
1698                 navigation_node::TYPE_SETTING, null, 'useroverrides');
1699     }
1701     if (has_capability('mod/quiz:manage', $PAGE->cm->context)) {
1702         $url = new moodle_url('/mod/quiz/edit.php', array('cmid'=>$PAGE->cm->id));
1703         $text = get_string('editquiz', 'quiz');
1704         $quiznode->add($text, $url, navigation_node::TYPE_SETTING, null,
1705                 'mod_quiz_edit', new pix_icon('t/edit', ''));
1706     }
1708     if (has_capability('mod/quiz:preview', $PAGE->cm->context)) {
1709         $url = new moodle_url('/mod/quiz/startattempt.php', array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey()));
1710         $quiznode->add(get_string('preview', 'quiz'), $url, navigation_node::TYPE_SETTING,
1711                 null, 'mod_quiz_preview', new pix_icon('t/preview', ''));
1712     }
1714     question_extend_settings_navigation($quiznode, $PAGE->cm->context)->trim_if_empty();
1717 /**
1718  * Serves the quiz files.
1719  *
1720  * @param object $course
1721  * @param object $cm
1722  * @param object $context
1723  * @param string $filearea
1724  * @param array $args
1725  * @param bool $forcedownload
1726  * @return bool false if file not found, does not return if found - justsend the file
1727  */
1728 function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload) {
1729     global $CFG, $DB;
1731     if ($context->contextlevel != CONTEXT_MODULE) {
1732         return false;
1733     }
1735     require_login($course, false, $cm);
1737     if (!$quiz = $DB->get_record('quiz', array('id'=>$cm->instance))) {
1738         return false;
1739     }
1741     // 'intro' area is served by pluginfile.php
1742     $fileareas = array('feedback');
1743     if (!in_array($filearea, $fileareas)) {
1744         return false;
1745     }
1747     $feedbackid = (int)array_shift($args);
1748     if (!$feedback = $DB->get_record('quiz_feedback', array('id'=>$feedbackid))) {
1749         return false;
1750     }
1752     $fs = get_file_storage();
1753     $relativepath = implode('/', $args);
1754     $fullpath = "/$context->id/mod_quiz/$filearea/$feedbackid/$relativepath";
1755     if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1756         return false;
1757     }
1758     send_stored_file($file, 0, 0, true);
1761 /**
1762  * Called via pluginfile.php -> question_pluginfile to serve files belonging to
1763  * a question in a question_attempt when that attempt is a quiz attempt.
1764  *
1765  * @param object $course course settings object
1766  * @param object $context context object
1767  * @param string $component the name of the component we are serving files for.
1768  * @param string $filearea the name of the file area.
1769  * @param array $args the remaining bits of the file path.
1770  * @param bool $forcedownload whether the user must be forced to download the file.
1771  * @return bool false if file not found, does not return if found - justsend the file
1772  */
1773 function quiz_question_pluginfile($course, $context, $component,
1774         $filearea, $attemptid, $questionid, $args, $forcedownload) {
1775     global $USER, $CFG;
1776     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
1778     $attemptobj = quiz_attempt::create($attemptid);
1779     require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
1780     $questionids = array($questionid);
1781     $attemptobj->load_questions($questionids);
1782     $attemptobj->load_question_states($questionids);
1784     if ($attemptobj->is_own_attempt() && !$attemptobj->is_finished()) {
1785         // In the middle of an attempt.
1786         if (!$attemptobj->is_preview_user()) {
1787             $attemptobj->require_capability('mod/quiz:attempt');
1788         }
1789         $isreviewing = false;
1791     } else {
1792         // Reviewing an attempt.
1793         $attemptobj->check_review_capability();
1794         $isreviewing = true;
1795     }
1797     if (!$attemptobj->check_file_access($questionid, $isreviewing, $context->id,
1798             $component, $filearea, $args, $forcedownload)) {
1799         send_file_not_found();
1800     }
1802     $fs = get_file_storage();
1803     $relativepath = implode('/', $args);
1804     $fullpath = "/$context->id/$component/$filearea/$relativepath";
1805     if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1806         send_file_not_found();
1807     }
1809     send_stored_file($file, 0, 0, $forcedownload);