weekly release 2.1dev
[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;
385     //datesubmitted == time created. dategraded == time modified or time overridden
386     //if grade was last modified by the user themselves use date graded. Otherwise use date submitted
387     //TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
388     if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
389         $result->time = $grade->dategraded;
390     } else {
391         $result->time = $grade->datesubmitted;
392     }
394     return $result;
397 /**
398  * Is this a graded quiz? If this method returns true, you can assume that
399  * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to
400  * divide by them).
401  *
402  * @param object $quiz a row from the quiz table.
403  * @return boolean whether this is a graded quiz.
404  */
405 function quiz_has_grades($quiz) {
406     return $quiz->grade != 0 && $quiz->sumgrades != 0;
409 /**
410  * Get the best current grade for a particular user in a quiz.
411  *
412  * @param object $quiz the quiz settings.
413  * @param integer $userid the id of the user.
414  * @return float the user's current grade for this quiz, or NULL if this user does
415  * not have a grade on this quiz.
416  */
417 function quiz_get_best_grade($quiz, $userid) {
418     global $DB;
419     $grade = $DB->get_field('quiz_grades', 'grade', array('quiz' => $quiz->id, 'userid' => $userid));
421     // Need to detect errors/no result, without catching 0 scores.
422     if ($grade === false) {
423         return null;
424     }
426     return $grade + 0; // Convert to number.
429 /**
430  * Print a detailed representation of what a  user has done with
431  * a given particular instance of this module, for user activity reports.
432  *
433  * @global object
434  * @param object $course
435  * @param object $user
436  * @param object $mod
437  * @param object $quiz
438  * @return bool
439  */
440 function quiz_user_complete($course, $user, $mod, $quiz) {
441     global $DB, $CFG, $OUTPUT;
442     require_once("$CFG->libdir/gradelib.php");
443     $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
444     if (!empty($grades->items[0]->grades)) {
445         $grade = reset($grades->items[0]->grades);
446         echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
447         if ($grade->str_feedback) {
448             echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
449         }
450     }
452     if ($attempts = $DB->get_records('quiz_attempts', array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) {
453         foreach ($attempts as $attempt) {
454             echo get_string('attempt', 'quiz').' '.$attempt->attempt.': ';
455             if ($attempt->timefinish == 0) {
456                 print_string('unfinished');
457             } else {
458                 echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' . quiz_format_grade($quiz, $quiz->sumgrades);
459             }
460             echo ' - '.userdate($attempt->timemodified).'<br />';
461         }
462     } else {
463        print_string('noattempts', 'quiz');
464     }
466     return true;
469 /**
470  * Function to be run periodically according to the moodle cron
471  * This function searches for things that need to be done, such
472  * as sending out mail, toggling flags etc ...
473  *
474  * @global stdClass
475  * @return bool true
476  */
477 function quiz_cron() {
478     global $CFG;
480     return true;
483 /**
484  * @global object
485  * @param integer $quizid the quiz id.
486  * @param integer $userid the userid.
487  * @param string $status 'all', 'finished' or 'unfinished' to control
488  * @param bool $includepreviews
489  * @return an array of all the user's attempts at this quiz. Returns an empty array if there are none.
490  */
491 function quiz_get_user_attempts($quizid, $userid=0, $status = 'finished', $includepreviews = false) {
492     global $DB;
493     $status_condition = array(
494         'all' => '',
495         'finished' => ' AND timefinish > 0',
496         'unfinished' => ' AND timefinish = 0'
497     );
498     $previewclause = '';
499     if (!$includepreviews) {
500         $previewclause = ' AND preview = 0';
501     }
502     $params=array($quizid);
503     if ($userid){
504         $userclause = ' AND userid = ?';
505         $params[]=$userid;
506     } else {
507         $userclause = '';
508     }
509     if ($attempts = $DB->get_records_select('quiz_attempts',
510             "quiz = ?" .$userclause. $previewclause . $status_condition[$status], $params,
511             'attempt ASC')) {
512         return $attempts;
513     } else {
514         return array();
515     }
518 /**
519  * Return grade for given user or all users.
520  *
521  * @global stdClass
522  * @global object
523  * @param int $quizid id of quiz
524  * @param int $userid optional user id, 0 means all users
525  * @return array array of grades, false if none. These are raw grades. They should
526  * be processed with quiz_format_grade for display.
527  */
528 function quiz_get_user_grades($quiz, $userid=0) {
529     global $CFG, $DB;
531     $params = array($quiz->id);
532     $wheresql = '';
533     if ($userid) {
534         $params[] = $userid;
535         $wheresql = "AND u.id = ?";
536     }
537     $sql = "SELECT u.id, u.id AS userid, g.grade AS rawgrade, g.timemodified AS dategraded, MAX(a.timefinish) AS datesubmitted
538             FROM {user} u, {quiz_grades} g, {quiz_attempts} a
539             WHERE u.id = g.userid AND g.quiz = ? AND a.quiz = g.quiz AND u.id = a.userid $wheresql
540             GROUP BY u.id, g.grade, g.timemodified";
542     return $DB->get_records_sql($sql, $params);
545 /**
546  * Round a grade to to the correct number of decimal places, and format it for display.
547  *
548  * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
549  * @param float $grade The grade to round.
550  * @return float
551  */
552 function quiz_format_grade($quiz, $grade) {
553     return format_float($grade, $quiz->decimalpoints);
556 /**
557  * Round a grade to to the correct number of decimal places, and format it for display.
558  *
559  * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
560  * @param float $grade The grade to round.
561  * @return float
562  */
563 function quiz_format_question_grade($quiz, $grade) {
564     if ($quiz->questiondecimalpoints == -1) {
565         return format_float($grade, $quiz->decimalpoints);
566     } else {
567         return format_float($grade, $quiz->questiondecimalpoints);
568     }
571 /**
572  * Update grades in central gradebook
573  *
574  * @global stdClass
575  * @global object
576  * @param object $quiz
577  * @param int $userid specific user only, 0 means all
578  */
579 function quiz_update_grades($quiz, $userid=0, $nullifnone=true) {
580     global $CFG, $DB;
581     require_once($CFG->libdir.'/gradelib.php');
583     if ($quiz->grade == 0) {
584         quiz_grade_item_update($quiz);
586     } else if ($grades = quiz_get_user_grades($quiz, $userid)) {
587         quiz_grade_item_update($quiz, $grades);
589     } else if ($userid and $nullifnone) {
590         $grade = new stdClass();
591         $grade->userid   = $userid;
592         $grade->rawgrade = NULL;
593         quiz_grade_item_update($quiz, $grade);
595     } else {
596         quiz_grade_item_update($quiz);
597     }
600 /**
601  * Update all grades in gradebook.
602  *
603  * @global object
604  */
605 function quiz_upgrade_grades() {
606     global $DB;
608     $sql = "SELECT COUNT('x')
609               FROM {quiz} a, {course_modules} cm, {modules} m
610              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
611     $count = $DB->count_records_sql($sql);
613     $sql = "SELECT a.*, cm.idnumber AS cmidnumber, a.course AS courseid
614               FROM {quiz} a, {course_modules} cm, {modules} m
615              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
616     $rs = $DB->get_recordset_sql($sql);
617     if ($rs->valid()) {
618         $pbar = new progress_bar('quizupgradegrades', 500, true);
619         $i=0;
620         foreach ($rs as $quiz) {
621             $i++;
622             upgrade_set_timeout(60*5); // set up timeout, may also abort execution
623             quiz_update_grades($quiz, 0, false);
624             $pbar->update($i, $count, "Updating Quiz grades ($i/$count).");
625         }
626     }
627     $rs->close();
630 /**
631  * Create grade item for given quiz
632  *
633  * @global stdClass
634  * @uses GRADE_TYPE_VALUE
635  * @uses GRADE_TYPE_NONE
636  * @uses QUIZ_REVIEW_SCORES
637  * @uses QUIZ_REVIEW_CLOSED
638  * @uses QUIZ_REVIEW_OPEN
639  * @uses PARAM_INT
640  * @uses GRADE_UPDATE_ITEM_LOCKED
641  * @param object $quiz object with extra cmidnumber
642  * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
643  * @return int 0 if ok, error code otherwise
644  */
645 function quiz_grade_item_update($quiz, $grades=NULL) {
646     global $CFG, $OUTPUT;
647     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
648         require_once($CFG->libdir.'/gradelib.php');
649     }
651     if (array_key_exists('cmidnumber', $quiz)) { //it may not be always present
652         $params = array('itemname'=>$quiz->name, 'idnumber'=>$quiz->cmidnumber);
653     } else {
654         $params = array('itemname'=>$quiz->name);
655     }
657     if ($quiz->grade > 0) {
658         $params['gradetype'] = GRADE_TYPE_VALUE;
659         $params['grademax']  = $quiz->grade;
660         $params['grademin']  = 0;
662     } else {
663         $params['gradetype'] = GRADE_TYPE_NONE;
664     }
666 /* description by TJ:
667 1/ If the quiz is set to not show scores while the quiz is still open, and is set to show scores after
668    the quiz is closed, then create the grade_item with a show-after date that is the quiz close date.
669 2/ If the quiz is set to not show scores at either of those times, create the grade_item as hidden.
670 3/ If the quiz is set to show scores, create the grade_item visible.
671 */
672     if (!($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
673     and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
674         $params['hidden'] = 1;
676     } else if ( ($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
677            and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
678         if ($quiz->timeclose) {
679             $params['hidden'] = $quiz->timeclose;
680         } else {
681             $params['hidden'] = 1;
682         }
684     } else {
685         // a) both open and closed enabled
686         // b) open enabled, closed disabled - we can not "hide after", grades are kept visible even after closing
687         $params['hidden'] = 0;
688     }
690     if ($grades  === 'reset') {
691         $params['reset'] = true;
692         $grades = NULL;
693     }
695     $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
696     if (!empty($gradebook_grades->items)) {
697         $grade_item = $gradebook_grades->items[0];
698         if ($grade_item->locked) {
699             $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
700             if (!$confirm_regrade) {
701                 $message = get_string('gradeitemislocked', 'grades');
702                 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id . '&amp;mode=overview';
703                 $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
704                 echo $OUTPUT->box_start('generalbox', 'notice');
705                 echo '<p>'. $message .'</p>';
706                 echo $OUTPUT->container_start('buttons');
707                 echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades'));
708                 echo $OUTPUT->single_button($back_link,  get_string('cancel'));
709                 echo $OUTPUT->container_end();
710                 echo $OUTPUT->box_end();
712                 return GRADE_UPDATE_ITEM_LOCKED;
713             }
714         }
715     }
717     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
720 /**
721  * Delete grade item for given quiz
722  *
723  * @global stdClass
724  * @param object $quiz object
725  * @return object quiz
726  */
727 function quiz_grade_item_delete($quiz) {
728     global $CFG;
729     require_once($CFG->libdir . '/gradelib.php');
731     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, NULL, array('deleted' => 1));
734 /**
735  * @return the options for calculating the quiz grade from the individual attempt grades.
736  */
737 function quiz_get_grading_options() {
738     return array (
739             QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'),
740             QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'),
741             QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'),
742             QUIZ_ATTEMPTLAST  => get_string('attemptlast', 'quiz'));
745 /**
746  * Returns an array of users who have data in a given quiz
747  *
748  * @todo: deprecated - to be deleted in 2.2
749  *
750  * @param int $quizid
751  * @return array
752  */
753 function quiz_get_participants($quizid) {
754     global $CFG, $DB;
756     //Get users from attempts
757     $us_attempts = $DB->get_records_sql("SELECT DISTINCT u.id, u.id
758                                     FROM {user} u,
759                                          {quiz_attempts} a
760                                     WHERE a.quiz = ? and
761                                           u.id = a.userid", array($quizid));
763     //Return us_attempts array (it contains an array of unique users)
764     return $us_attempts;
768 /**
769  * This standard function will check all instances of this module
770  * and make sure there are up-to-date events created for each of them.
771  * If courseid = 0, then every quiz event in the site is checked, else
772  * only quiz events belonging to the course specified are checked.
773  * This function is used, in its new format, by restore_refresh_events()
774  *
775  * @global object
776  * @uses QUIZ_MAX_EVENT_LENGTH
777  * @param int $courseid
778  * @return bool
779  */
780 function quiz_refresh_events($courseid = 0) {
781     global $DB;
783     if ($courseid == 0) {
784         if (! $quizzes = $DB->get_records('quiz')) {
785             return true;
786         }
787     } else {
788         if (! $quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
789             return true;
790         }
791     }
793     foreach ($quizzes as $quiz) {
794         quiz_update_events($quiz);
795     }
797     return true;
800 /**
801  * Returns all quiz graded users since a given time for specified quiz
802  */
803 function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
804         $courseid, $cmid, $userid = 0, $groupid = 0) {
805     global $CFG, $COURSE, $USER, $DB;
806     require_once('locallib.php');
808     if ($COURSE->id == $courseid) {
809         $course = $COURSE;
810     } else {
811         $course = $DB->get_record('course', array('id' => $courseid));
812     }
814     $modinfo =& get_fast_modinfo($course);
816     $cm = $modinfo->cms[$cmid];
817     $quiz = $DB->get_record('quiz', array('id' => $cm->instance));
819     if ($userid) {
820         $userselect = "AND u.id = :userid";
821         $params['userid'] = $userid;
822     } else {
823         $userselect = '';
824     }
826     if ($groupid) {
827         $groupselect = 'AND gm.groupid = :groupid';
828         $groupjoin   = 'JOIN {groups_members} gm ON  gm.userid=u.id';
829         $params['groupid'] = $groupid;
830     } else {
831         $groupselect = '';
832         $groupjoin   = '';
833     }
835     $params['timestart'] = $timestart;
836     $params['quizid'] = $quiz->id;
838     if (!$attempts = $DB->get_records_sql("
839               SELECT qa.*,
840                      u.firstname, u.lastname, u.email, u.picture, u.imagealt
841                 FROM {quiz_attempts} qa
842                      JOIN {user} u ON u.id = qa.userid
843                      $groupjoin
844                WHERE qa.timefinish > :timestart
845                  AND qa.quiz = :quizid
846                  AND qa.preview = 0
847                      $userselect
848                      $groupselect
849             ORDER BY qa.timefinish ASC", $params)) {
850         return;
851     }
853     $context         = get_context_instance(CONTEXT_MODULE, $cm->id);
854     $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
855     $viewfullnames   = has_capability('moodle/site:viewfullnames', $context);
856     $grader          = has_capability('mod/quiz:viewreports', $context);
857     $groupmode       = groups_get_activity_groupmode($cm, $course);
859     if (is_null($modinfo->groups)) {
860         $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
861     }
863     $usersgroups = null;
864     $aname = format_string($cm->name,true);
865     foreach ($attempts as $attempt) {
866         if ($attempt->userid != $USER->id) {
867             if (!$grader) {
868                 // Grade permission required
869                 continue;
870             }
872             if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
873                 if (is_null($usersgroups)) {
874                     $usersgroups = groups_get_all_groups($course->id,
875                             $attempt->userid, $cm->groupingid);
876                     if (is_array($usersgroups)) {
877                         $usersgroups = array_keys($usersgroups);
878                     } else {
879                         $usersgroups = array();
880                     }
881                 }
882                 if (!array_intersect($usersgroups, $modinfo->groups[$cm->id])) {
883                     continue;
884                 }
885             }
886         }
888         $options = quiz_get_reviewoptions($quiz, $attempt, $context);
890         $tmpactivity = new stdClass;
892         $tmpactivity->type       = 'quiz';
893         $tmpactivity->cmid       = $cm->id;
894         $tmpactivity->name       = $aname;
895         $tmpactivity->sectionnum = $cm->sectionnum;
896         $tmpactivity->timestamp  = $attempt->timefinish;
898         $tmpactivity->content->attemptid = $attempt->id;
899         $tmpactivity->content->attempt   = $attempt->attempt;
900         if (quiz_has_grades($quiz) && $options->scores) {
901             $tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades);
902             $tmpactivity->content->maxgrade  = quiz_format_grade($quiz, $quiz->sumgrades);
903         } else {
904             $tmpactivity->content->sumgrades = null;
905             $tmpactivity->content->maxgrade  = null;
906         }
908         $tmpactivity->user->id        = $attempt->userid;
909         $tmpactivity->user->firstname = $attempt->firstname;
910         $tmpactivity->user->lastname = $attempt->lastname;
911         $tmpactivity->user->fullname = fullname($attempt, $viewfullnames);
912         $tmpactivity->user->picture  = $attempt->picture;
913         $tmpactivity->user->imagealt = $attempt->imagealt;
914         $tmpactivity->user->email = $attempt->email;
916         $activities[$index++] = $tmpactivity;
917     }
919   return;
922 function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
923     global $CFG, $OUTPUT;
925     echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">';
927     echo '<tr><td class="userpicture" valign="top">';
928     echo $OUTPUT->user_picture($activity->user, array('courseid' => $courseid));
929     echo '</td><td>';
931     if ($detail) {
932         $modname = $modnames[$activity->type];
933         echo '<div class="title">';
934         echo '<img src="' . $OUTPUT->pix_url('icon', $activity->type) . '" ' .
935                 'class="icon" alt="' . $modname . '" />';
936         echo '<a href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
937                 $activity->cmid . '">' . $activity->name . '</a>';
938         echo '</div>';
939     }
941     echo '<div class="grade">';
942     echo  get_string('attempt', 'quiz', $activity->content->attempt);
943     if (isset($activity->content->maxgrade)) {
944         $grades = $activity->content->sumgrades . ' / ' . $activity->content->maxgrade;
945         echo ': (<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?attempt=' .
946                 $activity->content->attemptid . '">' . $grades . '</a>)';
947     }
948     echo '</div>';
950     echo '<div class="user">';
951     echo '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $activity->user->id .
952             '&amp;course=' . $courseid . '">' . $activity->user->fullname .
953             '</a> - ' . userdate($activity->timestamp);
954     echo '</div>';
956     echo '</td></tr></table>';
958     return;
961 /**
962  * Pre-process the quiz options form data, making any necessary adjustments.
963  * Called by add/update instance in this file.
964  *
965  * @uses QUIZ_REVIEW_OVERALLFEEDBACK
966  * @uses QUIZ_REVIEW_CLOSED
967  * @uses QUIZ_REVIEW_OPEN
968  * @uses QUIZ_REVIEW_IMMEDIATELY
969  * @uses QUIZ_REVIEW_GENERALFEEDBACK
970  * @uses QUIZ_REVIEW_SOLUTIONS
971  * @uses QUIZ_REVIEW_ANSWERS
972  * @uses QUIZ_REVIEW_FEEDBACK
973  * @uses QUIZ_REVIEW_SCORES
974  * @uses QUIZ_REVIEW_RESPONSES
975  * @uses QUESTION_ADAPTIVE
976  * @param object $quiz The variables set on the form.
977  * @return string
978  */
979 function quiz_process_options(&$quiz) {
980     $quiz->timemodified = time();
982     // Quiz name.
983     if (!empty($quiz->name)) {
984         $quiz->name = trim($quiz->name);
985     }
987     // Password field - different in form to stop browsers that remember passwords
988     // getting confused.
989     $quiz->password = $quiz->quizpassword;
990     unset($quiz->quizpassword);
992     // Quiz feedback
993     if (isset($quiz->feedbacktext)) {
994         // Clean up the boundary text.
995         for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
996             if (empty($quiz->feedbacktext[$i]['text'])) {
997                 $quiz->feedbacktext[$i]['text'] = '';
998             } else {
999                 $quiz->feedbacktext[$i]['text'] = trim($quiz->feedbacktext[$i]['text']);
1000             }
1001         }
1003         // Check the boundary value is a number or a percentage, and in range.
1004         $i = 0;
1005         while (!empty($quiz->feedbackboundaries[$i])) {
1006             $boundary = trim($quiz->feedbackboundaries[$i]);
1007             if (!is_numeric($boundary)) {
1008                 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
1009                     $boundary = trim(substr($boundary, 0, -1));
1010                     if (is_numeric($boundary)) {
1011                         $boundary = $boundary * $quiz->grade / 100.0;
1012                     } else {
1013                         return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
1014                     }
1015                 }
1016             }
1017             if ($boundary <= 0 || $boundary >= $quiz->grade) {
1018                 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
1019             }
1020             if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
1021                 return get_string('feedbackerrororder', 'quiz', $i + 1);
1022             }
1023             $quiz->feedbackboundaries[$i] = $boundary;
1024             $i += 1;
1025         }
1026         $numboundaries = $i;
1028         // Check there is nothing in the remaining unused fields.
1029         if (!empty($quiz->feedbackboundaries)) {
1030             for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
1031                 if (!empty($quiz->feedbackboundaries[$i]) && trim($quiz->feedbackboundaries[$i]) != '') {
1032                     return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
1033                 }
1034             }
1035         }
1036         for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
1037             if (!empty($quiz->feedbacktext[$i]['text']) && trim($quiz->feedbacktext[$i]['text']) != '') {
1038                 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
1039             }
1040         }
1041         $quiz->feedbackboundaries[-1] = $quiz->grade + 1; // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
1042         $quiz->feedbackboundaries[$numboundaries] = 0;
1043         $quiz->feedbackboundarycount = $numboundaries;
1044     }
1046     // Settings that get combined to go into the optionflags column.
1047     $quiz->optionflags = 0;
1048     if (!empty($quiz->adaptive)) {
1049         $quiz->optionflags |= QUESTION_ADAPTIVE;
1050     }
1052     // Settings that get combined to go into the review column.
1053     $review = 0;
1054     if (isset($quiz->responsesimmediately)) {
1055         $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY);
1056         unset($quiz->responsesimmediately);
1057     }
1058     if (isset($quiz->responsesopen)) {
1059         $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN);
1060         unset($quiz->responsesopen);
1061     }
1062     if (isset($quiz->responsesclosed)) {
1063         $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED);
1064         unset($quiz->responsesclosed);
1065     }
1067     if (isset($quiz->scoreimmediately)) {
1068         $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY);
1069         unset($quiz->scoreimmediately);
1070     }
1071     if (isset($quiz->scoreopen)) {
1072         $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN);
1073         unset($quiz->scoreopen);
1074     }
1075     if (isset($quiz->scoreclosed)) {
1076         $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED);
1077         unset($quiz->scoreclosed);
1078     }
1080     if (isset($quiz->feedbackimmediately)) {
1081         $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1082         unset($quiz->feedbackimmediately);
1083     }
1084     if (isset($quiz->feedbackopen)) {
1085         $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN);
1086         unset($quiz->feedbackopen);
1087     }
1088     if (isset($quiz->feedbackclosed)) {
1089         $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED);
1090         unset($quiz->feedbackclosed);
1091     }
1093     if (isset($quiz->answersimmediately)) {
1094         $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
1095         unset($quiz->answersimmediately);
1096     }
1097     if (isset($quiz->answersopen)) {
1098         $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN);
1099         unset($quiz->answersopen);
1100     }
1101     if (isset($quiz->answersclosed)) {
1102         $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED);
1103         unset($quiz->answersclosed);
1104     }
1106     if (isset($quiz->solutionsimmediately)) {
1107         $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_IMMEDIATELY);
1108         unset($quiz->solutionsimmediately);
1109     }
1110     if (isset($quiz->solutionsopen)) {
1111         $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_OPEN);
1112         unset($quiz->solutionsopen);
1113     }
1114     if (isset($quiz->solutionsclosed)) {
1115         $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_CLOSED);
1116         unset($quiz->solutionsclosed);
1117     }
1119     if (isset($quiz->generalfeedbackimmediately)) {
1120         $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1121         unset($quiz->generalfeedbackimmediately);
1122     }
1123     if (isset($quiz->generalfeedbackopen)) {
1124         $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_OPEN);
1125         unset($quiz->generalfeedbackopen);
1126     }
1127     if (isset($quiz->generalfeedbackclosed)) {
1128         $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_CLOSED);
1129         unset($quiz->generalfeedbackclosed);
1130     }
1132     if (isset($quiz->overallfeedbackimmediately)) {
1133         $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1134         unset($quiz->overallfeedbackimmediately);
1135     }
1136     if (isset($quiz->overallfeedbackopen)) {
1137         $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_OPEN);
1138         unset($quiz->overallfeedbackopen);
1139     }
1140     if (isset($quiz->overallfeedbackclosed)) {
1141         $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_CLOSED);
1142         unset($quiz->overallfeedbackclosed);
1143     }
1145     $quiz->review = $review;
1148 /**
1149  * This function is called at the end of quiz_add_instance
1150  * and quiz_update_instance, to do the common processing.
1151  *
1152  * @global object
1153  * @uses QUIZ_MAX_EVENT_LENGTH
1154  * @param object $quiz the quiz object.
1155  * @return void|string Void or error message
1156  */
1157 function quiz_after_add_or_update($quiz) {
1158     global $DB;
1159     $cmid = $quiz->coursemodule;
1161     // we need to use context now, so we need to make sure all needed info is already in db
1162     $DB->set_field('course_modules', 'instance', $quiz->id, array('id'=>$cmid));
1163     $context = get_context_instance(CONTEXT_MODULE, $cmid);
1165     // Save the feedback
1166     $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
1168     for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) {
1169         $feedback = new stdClass;
1170         $feedback->quizid = $quiz->id;
1171         $feedback->feedbacktext = $quiz->feedbacktext[$i]['text'];
1172         $feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format'];
1173         $feedback->mingrade = $quiz->feedbackboundaries[$i];
1174         $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
1175         $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1176         $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']);
1177         $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext, array('id'=>$feedback->id));
1178     }
1180     // Update the events relating to this quiz.
1181     quiz_update_events($quiz);
1183     //update related grade item
1184     quiz_grade_item_update($quiz);
1188 /**
1189  * This function updates the events associated to the quiz.
1190  * If $override is non-zero, then it updates only the events
1191  * associated with the specified override.
1192  *
1193  * @uses QUIZ_MAX_EVENT_LENGTH
1194  * @param object $quiz the quiz object.
1195  * @param object optional $override limit to a specific override
1196  */
1197 function quiz_update_events($quiz, $override = null) {
1198     global $DB;
1200     // Load the old events relating to this quiz.
1201     $conds = array('modulename'=>'quiz',
1202                    'instance'=>$quiz->id);
1203     if (!empty($override)) {
1204         // only load events for this override
1205         $conds['groupid'] = isset($override->groupid)?  $override->groupid : 0;
1206         $conds['userid'] = isset($override->userid)?  $override->userid : 0;
1207     }
1208     $oldevents = $DB->get_records('event', $conds);
1210     // Now make a todo list of all that needs to be updated
1211     if (empty($override)) {
1212         // We are updating the primary settings for the quiz, so we
1213         // need to add all the overrides
1214         $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id));
1215         // as well as the original quiz (empty override)
1216         $overrides[] = new stdClass;
1217     }
1218     else {
1219         // Just do the one override
1220         $overrides = array($override);
1221     }
1223     foreach ($overrides as $current) {
1224         $groupid   = isset($current->groupid)?  $current->groupid : 0;
1225         $userid    = isset($current->userid)? $current->userid : 0;
1226         $timeopen  = isset($current->timeopen)?  $current->timeopen : $quiz->timeopen;
1227         $timeclose = isset($current->timeclose)? $current->timeclose : $quiz->timeclose;
1229         // only add open/close events for an override if they differ from the quiz default
1230         $addopen  = empty($current->id) || !empty($current->timeopen);
1231         $addclose = empty($current->id) || !empty($current->timeclose);
1233         $event = new stdClass;
1234         $event->description = $quiz->intro;
1235         $event->courseid    = ($userid) ? 0 : $quiz->course; // Events module won't show user events when the courseid is nonzero
1236         $event->groupid     = $groupid;
1237         $event->userid      = $userid;
1238         $event->modulename  = 'quiz';
1239         $event->instance    = $quiz->id;
1240         $event->timestart   = $timeopen;
1241         $event->timeduration = max($timeclose - $timeopen, 0);
1242         $event->visible     = instance_is_visible('quiz', $quiz);
1243         $event->eventtype   = 'open';
1245         // Determine the event name
1246         if ($groupid) {
1247             $params = new stdClass;
1248             $params->quiz = $quiz->name;
1249             $params->group = groups_get_group_name($groupid);
1250             if ($params->group === false) {
1251                 // group doesn't exist, just skip it
1252                 continue;
1253             }
1254             $eventname = get_string('overridegroupeventname', 'quiz', $params);
1255         }
1256         else if ($userid) {
1257             $params = new stdClass;
1258             $params->quiz = $quiz->name;
1259             $eventname = get_string('overrideusereventname', 'quiz', $params);
1260         } else {
1261             $eventname = $quiz->name;
1262         }
1263         if ($addopen or $addclose) {
1264             if ($timeclose and $timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) {
1265                 // Single event for the whole quiz.
1266                 if ($oldevent = array_shift($oldevents)) {
1267                     $event->id = $oldevent->id;
1268                 }
1269                 else {
1270                     unset($event->id);
1271                 }
1272                 $event->name = $eventname;
1273                 // calendar_event::create will reuse a db record if the id field is set
1274                 calendar_event::create($event);
1275             } else {
1276                 // Separate start and end events.
1277                 $event->timeduration  = 0;
1278                 if ($timeopen && $addopen) {
1279                     if ($oldevent = array_shift($oldevents)) {
1280                         $event->id = $oldevent->id;
1281                     }
1282                     else {
1283                         unset($event->id);
1284                     }
1285                     $event->name = $eventname.' ('.get_string('quizopens', 'quiz').')';
1286                     // calendar_event::create will reuse a db record if the id field is set
1287                     calendar_event::create($event);
1288                 }
1289                 if ($timeclose && $addclose) {
1290                     if ($oldevent = array_shift($oldevents)) {
1291                         $event->id = $oldevent->id;
1292                     }
1293                     else {
1294                         unset($event->id);
1295                     }
1296                     $event->name      = $eventname.' ('.get_string('quizcloses', 'quiz').')';
1297                     $event->timestart = $timeclose;
1298                     $event->eventtype = 'close';
1299                     calendar_event::create($event);
1300                 }
1301             }
1302         }
1303     }
1305     // Delete any leftover events
1306     foreach ($oldevents as $badevent) {
1307         $badevent = calendar_event::load($badevent);
1308         $badevent->delete();
1309     }
1312 /**
1313  * @return array
1314  */
1315 function quiz_get_view_actions() {
1316     return array('view', 'view all', 'report', 'review');
1319 /**
1320  * @return array
1321  */
1322 function quiz_get_post_actions() {
1323     return array('attempt', 'close attempt', 'preview', 'editquestions', 'delete attempt', 'manualgrade');
1326 /**
1327  * Returns an array of names of quizzes that use this question
1328  *
1329  * @param integer $questionid
1330  * @return array of strings
1331  */
1332 function quiz_question_list_instances($questionid) {
1333     global $CFG, $DB;
1335     // TODO MDL-5780: we should also consider other questions that are used by
1336     // random questions in this quiz, but that is very hard.
1338     $sql = "SELECT q.id, q.name
1339             FROM {quiz} q
1340             JOIN {quiz_question_instances} qqi ON q.id = qqi.quiz
1341             WHERE qqi.question = ?";
1343     if ($instances = $DB->get_records_sql_menu($sql, array($questionid))) {
1344         return $instances;
1345     }
1346     return array();
1349 /**
1350  * Implementation of the function for printing the form elements that control
1351  * whether the course reset functionality affects the quiz.
1352  *
1353  * @param $mform form passed by reference
1354  */
1355 function quiz_reset_course_form_definition(&$mform) {
1356     $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz'));
1357     $mform->addElement('advcheckbox', 'reset_quiz_attempts', get_string('removeallquizattempts','quiz'));
1360 /**
1361  * Course reset form defaults.
1362  * @return array
1363  */
1364 function quiz_reset_course_form_defaults($course) {
1365     return array('reset_quiz_attempts'=>1);
1368 /**
1369  * Removes all grades from gradebook
1370  *
1371  * @global stdClass
1372  * @global object
1373  * @param int $courseid
1374  * @param string optional type
1375  */
1376 function quiz_reset_gradebook($courseid, $type='') {
1377     global $CFG, $DB;
1379     $sql = "SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
1380               FROM {quiz} q, {course_modules} cm, {modules} m
1381              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=q.id AND q.course=?";
1383     if ($quizs = $DB->get_records_sql($sql, array($courseid))) {
1384         foreach ($quizs as $quiz) {
1385             quiz_grade_item_update($quiz, 'reset');
1386         }
1387     }
1390 /**
1391  * Actual implementation of the reset course functionality, delete all the
1392  * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is
1393  * set and true.
1394  *
1395  * Also, move the quiz open and close dates, if the course start date is changing.
1396  *
1397  * @global stdClass
1398  * @global object
1399  * @param object $data the data submitted from the reset course.
1400  * @return array status array
1401  */
1402 function quiz_reset_userdata($data) {
1403     global $CFG, $DB;
1404     require_once($CFG->libdir.'/questionlib.php');
1406     $componentstr = get_string('modulenameplural', 'quiz');
1407     $status = array();
1409     /// Delete attempts.
1410     if (!empty($data->reset_quiz_attempts)) {
1411         $quizzes = $DB->get_records('quiz', array('course' => $data->courseid));
1412         foreach ($quizzes as $quiz) {
1413             quiz_delete_all_attempts($quiz);
1414         }
1416         // remove all grades from gradebook
1417         if (empty($data->reset_gradebook_grades)) {
1418             quiz_reset_gradebook($data->courseid);
1419         }
1420         $status[] = array('component' => $componentstr, 'item' => get_string('attemptsdeleted', 'quiz'), 'error' => false);
1421     }
1423     /// updating dates - shift may be negative too
1424     if ($data->timeshift) {
1425         shift_course_mod_dates('quiz', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
1426         $status[] = array('component' => $componentstr, 'item' => get_string('openclosedatesupdated', 'quiz'), 'error' => false);
1427     }
1429     return $status;
1432 /**
1433  * Checks whether the current user is allowed to view a file uploaded in a quiz.
1434  * Teachers can view any from their courses, students can only view their own.
1435  *
1436  * @global object
1437  * @global object
1438  * @uses CONTEXT_COURSE
1439  * @param int $attemptuniqueid int attempt id
1440  * @param int $questionid int question id
1441  * @return boolean to indicate access granted or denied
1442  */
1443 function quiz_check_file_access($attemptuniqueid, $questionid, $context = null) {
1444     global $USER, $DB, $CFG;
1445     require_once(dirname(__FILE__).'/attemptlib.php');
1446     require_once(dirname(__FILE__).'/locallib.php');
1448     $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $attemptuniqueid));
1449     $attemptobj = quiz_attempt::create($attempt->id);
1451     // does question exist?
1452     if (!$question = $DB->get_record('question', array('id' => $questionid))) {
1453         return false;
1454     }
1456     if ($context === null) {
1457         $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz));
1458         $cm = get_coursemodule_from_id('quiz', $quiz->id);
1459         $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1460     }
1462     // Load those questions and the associated states.
1463     $attemptobj->load_questions(array($questionid));
1464     $attemptobj->load_question_states(array($questionid));
1466     // obtain state
1467     $state = $attemptobj->get_question_state($questionid);
1468     // obtain questoin
1469     $question = $attemptobj->get_question($questionid);
1471     // access granted if the current user submitted this file
1472     if ($attempt->userid != $USER->id) {
1473         return false;
1474     // access granted if the current user has permission to grade quizzes in this course
1475     }
1476     if (!(has_capability('mod/quiz:viewreports', $context) || has_capability('mod/quiz:grade', $context))) {
1477         return false;
1478     }
1480     return array($question, $state, array());
1483 /**
1484  * Prints quiz summaries on MyMoodle Page
1485  *
1486  * @global object
1487  * @global object
1488  * @param arry $courses
1489  * @param array $htmlarray
1490  */
1491 function quiz_print_overview($courses, &$htmlarray) {
1492     global $USER, $CFG;
1493 /// These next 6 Lines are constant in all modules (just change module name)
1494     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1495         return array();
1496     }
1498     if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
1499         return;
1500     }
1502 /// Fetch some language strings outside the main loop.
1503     $strquiz = get_string('modulename', 'quiz');
1504     $strnoattempts = get_string('noattempts', 'quiz');
1506 /// We want to list quizzes that are currently available, and which have a close date.
1507 /// This is the same as what the lesson does, and the dabate is in MDL-10568.
1508     $now = time();
1509     foreach ($quizzes as $quiz) {
1510         if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
1511         /// Give a link to the quiz, and the deadline.
1512             $str = '<div class="quiz overview">' .
1513                     '<div class="name">' . $strquiz . ': <a ' . ($quiz->visible ? '' : ' class="dimmed"') .
1514                     ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->coursemodule . '">' .
1515                     $quiz->name . '</a></div>';
1516             $str .= '<div class="info">' . get_string('quizcloseson', 'quiz', userdate($quiz->timeclose)) . '</div>';
1518         /// Now provide more information depending on the uers's role.
1519             $context = get_context_instance(CONTEXT_MODULE, $quiz->coursemodule);
1520             if (has_capability('mod/quiz:viewreports', $context)) {
1521             /// For teacher-like people, show a summary of the number of student attempts.
1522                 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
1523                 // fields set to make the following call work.
1524                 $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
1525             } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student
1526             /// For student-like people, tell them how many attempts they have made.
1527                 if (isset($USER->id) && ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
1528                     $numattempts = count($attempts);
1529                     $str .= '<div class="info">' . get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
1530                 } else {
1531                     $str .= '<div class="info">' . $strnoattempts . '</div>';
1532                 }
1533             } else {
1534             /// For ayone else, there is no point listing this quiz, so stop processing.
1535                 continue;
1536             }
1538         /// Add the output for this quiz to the rest.
1539             $str .= '</div>';
1540             if (empty($htmlarray[$quiz->course]['quiz'])) {
1541                 $htmlarray[$quiz->course]['quiz'] = $str;
1542             } else {
1543                 $htmlarray[$quiz->course]['quiz'] .= $str;
1544             }
1545         }
1546     }
1549 /**
1550  * Return a textual summary of the number of attemtps that have been made at a particular quiz,
1551  * returns '' if no attemtps have been made yet, unless $returnzero is passed as true.
1552  *
1553  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1554  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
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 a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
1559  *          "Attemtps 123 (45 from this group)".
1560  */
1561 function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
1562     global $DB, $USER;
1563     $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0));
1564     if ($numattempts || $returnzero) {
1565         if (groups_get_activity_groupmode($cm)) {
1566             $a->total = $numattempts;
1567             if ($currentgroup) {
1568                 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1569                         '{quiz_attempts} qa JOIN ' .
1570                         '{groups_members} gm ON qa.userid = gm.userid ' .
1571                         'WHERE quiz = ? AND preview = 0 AND groupid = ?', array($quiz->id, $currentgroup));
1572                 return get_string('attemptsnumthisgroup', 'quiz', $a);
1573             } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
1574                 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups));
1575                 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1576                         '{quiz_attempts} qa JOIN ' .
1577                         '{groups_members} gm ON qa.userid = gm.userid ' .
1578                         'WHERE quiz = ? AND preview = 0 AND ' .
1579                         "groupid $usql", array_merge(array($quiz->id), $params));
1580                 return get_string('attemptsnumyourgroups', 'quiz', $a);
1581             }
1582         }
1583         return get_string('attemptsnum', 'quiz', $numattempts);
1584     }
1585     return '';
1588 /**
1589  * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
1590  * to the quiz reports.
1591  *
1592  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1593  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
1594  * @param object $context the quiz context.
1595  * @param boolean $returnzero if false (default), when no attempts have been made '' is returned instead of 'Attempts: 0'.
1596  * @param int $currentgroup if there is a concept of current group where this method is being called
1597  *         (e.g. a report) pass it in here. Default 0 which means no current group.
1598  * @return string HTML fragment for the link.
1599  */
1600 function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false, $currentgroup = 0) {
1601     global $CFG;
1602     $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
1603     if (!$summary) {
1604         return '';
1605     }
1607     require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1608     $url = new moodle_url('/mod/quiz/report.php', array(
1609             'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
1610     return html_writer::link($url, $summary);
1613 /**
1614  * @param string $feature FEATURE_xx constant for requested feature
1615  * @return bool True if quiz supports feature
1616  */
1617 function quiz_supports($feature) {
1618     switch($feature) {
1619         case FEATURE_GROUPS:                  return true;
1620         case FEATURE_GROUPINGS:               return true;
1621         case FEATURE_GROUPMEMBERSONLY:        return true;
1622         case FEATURE_MOD_INTRO:               return true;
1623         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
1624         case FEATURE_GRADE_HAS_GRADE:         return true;
1625         case FEATURE_GRADE_OUTCOMES:          return true;
1626         case FEATURE_BACKUP_MOODLE2:          return true;
1628         default: return null;
1629     }
1632 /**
1633  * @global object
1634  * @global stdClass
1635  * @return array all other caps used in module
1636  */
1637 function quiz_get_extra_capabilities() {
1638     global $CFG;
1639     require_once($CFG->libdir.'/questionlib.php');
1640     $caps = question_get_all_capabilities();
1641     $caps[] = 'moodle/site:accessallgroups';
1642     return $caps;
1645 /**
1646  * This fucntion extends the global navigation for the site.
1647  * It is important to note that you should not rely on PAGE objects within this
1648  * body of code as there is no guarantee that during an AJAX request they are
1649  * available
1650  *
1651  * @param navigation_node $quiznode The quiz node within the global navigation
1652  * @param stdClass $course The course object returned from the DB
1653  * @param stdClass $module The module object returned from the DB
1654  * @param stdClass $cm The course module instance returned from the DB
1655  */
1656 function quiz_extend_navigation($quiznode, $course, $module, $cm) {
1657     global $CFG;
1659     $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1661     if (has_capability('mod/quiz:view', $context)) {
1662         $url = new moodle_url('/mod/quiz/view.php', array('id'=>$cm->id));
1663         $quiznode->add(get_string('info', 'quiz'), $url, navigation_node::TYPE_SETTING,
1664                 null, null, new pix_icon('i/info', ''));
1665     }
1667     if (has_capability('mod/quiz:viewreports', $context)) {
1668         require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php');
1669         $reportlist = quiz_report_list($context);
1671         $url = new moodle_url('/mod/quiz/report.php', array('id' => $cm->id, 'mode' => reset($reportlist)));
1672         $reportnode = $quiznode->add(get_string('results', 'quiz'), $url, navigation_node::TYPE_SETTING,
1673                 null, null, new pix_icon('i/report', ''));
1675         foreach ($reportlist as $report) {
1676             $url = new moodle_url('/mod/quiz/report.php', array('id' => $cm->id, 'mode' => $report));
1677             $reportnode->add(get_string($report, 'quiz_'.$report), $url, navigation_node::TYPE_SETTING,
1678                     null, 'quiz_report_' . $report, new pix_icon('i/item', ''));
1679         }
1680     }
1683 /**
1684  * This function extends the settings navigation block for the site.
1685  *
1686  * It is safe to rely on PAGE here as we will only ever be within the module
1687  * context when this is called
1688  *
1689  * @param settings_navigation $settings
1690  * @param navigation_node $quiznode
1691  */
1692 function quiz_extend_settings_navigation($settings, $quiznode) {
1693     global $PAGE, $CFG;
1695     /**
1696      * Require {@link questionlib.php}
1697      * Included here as we only ever want to include this file if we really need to.
1698      */
1699     require_once($CFG->libdir . '/questionlib.php');
1701     if (has_capability('mod/quiz:manageoverrides', $PAGE->cm->context)) {
1702         $url = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$PAGE->cm->id));
1703         $quiznode->add(get_string('groupoverrides', 'quiz'), new moodle_url($url, array('mode'=>'group')),
1704                 navigation_node::TYPE_SETTING, null, 'groupoverrides');
1705         $quiznode->add(get_string('useroverrides', 'quiz'), new moodle_url($url, array('mode'=>'user')),
1706                 navigation_node::TYPE_SETTING, null, 'useroverrides');
1707     }
1709     if (has_capability('mod/quiz:manage', $PAGE->cm->context)) {
1710         $url = new moodle_url('/mod/quiz/edit.php', array('cmid'=>$PAGE->cm->id));
1711         $text = get_string('editquiz', 'quiz');
1712         $quiznode->add($text, $url, navigation_node::TYPE_SETTING, null,
1713                 'mod_quiz_edit', new pix_icon('t/edit', ''));
1714     }
1716     if (has_capability('mod/quiz:preview', $PAGE->cm->context)) {
1717         $url = new moodle_url('/mod/quiz/startattempt.php', array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey()));
1718         $quiznode->add(get_string('preview', 'quiz'), $url, navigation_node::TYPE_SETTING,
1719                 null, 'mod_quiz_preview', new pix_icon('t/preview', ''));
1720     }
1722     question_extend_settings_navigation($quiznode, $PAGE->cm->context)->trim_if_empty();
1725 /**
1726  * Serves the quiz files.
1727  *
1728  * @param object $course
1729  * @param object $cm
1730  * @param object $context
1731  * @param string $filearea
1732  * @param array $args
1733  * @param bool $forcedownload
1734  * @return bool false if file not found, does not return if found - justsend the file
1735  */
1736 function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload) {
1737     global $CFG, $DB;
1739     if ($context->contextlevel != CONTEXT_MODULE) {
1740         return false;
1741     }
1743     require_login($course, false, $cm);
1745     if (!$quiz = $DB->get_record('quiz', array('id'=>$cm->instance))) {
1746         return false;
1747     }
1749     // 'intro' area is served by pluginfile.php
1750     $fileareas = array('feedback');
1751     if (!in_array($filearea, $fileareas)) {
1752         return false;
1753     }
1755     $feedbackid = (int)array_shift($args);
1756     if (!$feedback = $DB->get_record('quiz_feedback', array('id'=>$feedbackid))) {
1757         return false;
1758     }
1760     $fs = get_file_storage();
1761     $relativepath = implode('/', $args);
1762     $fullpath = "/$context->id/mod_quiz/$filearea/$feedbackid/$relativepath";
1763     if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1764         return false;
1765     }
1766     send_stored_file($file, 0, 0, true);
1769 /**
1770  * Called via pluginfile.php -> question_pluginfile to serve files belonging to
1771  * a question in a question_attempt when that attempt is a quiz attempt.
1772  *
1773  * @param object $course course settings object
1774  * @param object $context context object
1775  * @param string $component the name of the component we are serving files for.
1776  * @param string $filearea the name of the file area.
1777  * @param array $args the remaining bits of the file path.
1778  * @param bool $forcedownload whether the user must be forced to download the file.
1779  * @return bool false if file not found, does not return if found - justsend the file
1780  */
1781 function quiz_question_pluginfile($course, $context, $component,
1782         $filearea, $uniqueid, $questionid, $args, $forcedownload) {
1783     global $USER, $CFG;
1784     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
1786     $attemptobj = quiz_attempt::create_from_unique_id($uniqueid);
1787     require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
1788     $questionids = array($questionid);
1789     $attemptobj->load_questions($questionids);
1790     $attemptobj->load_question_states($questionids);
1792     if ($attemptobj->is_own_attempt() && !$attemptobj->is_finished()) {
1793         // In the middle of an attempt.
1794         if (!$attemptobj->is_preview_user()) {
1795             $attemptobj->require_capability('mod/quiz:attempt');
1796         }
1797         $isreviewing = false;
1799     } else {
1800         // Reviewing an attempt.
1801         $attemptobj->check_review_capability();
1802         $isreviewing = true;
1803     }
1805     if (!$attemptobj->check_file_access($questionid, $isreviewing, $context->id,
1806             $component, $filearea, $args, $forcedownload)) {
1807         send_file_not_found();
1808     }
1810     $fs = get_file_storage();
1811     $relativepath = implode('/', $args);
1812     $fullpath = "/$context->id/$component/$filearea/$relativepath";
1813     if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1814         send_file_not_found();
1815     }
1817     send_stored_file($file, 0, 0, $forcedownload);