6846aef06f180065ae07669556c24f13ad3591e0
[moodle.git] / mod / quiz / lib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Library of functions for the quiz module.
19  *
20  * This contains functions that are called also from outside the quiz module
21  * Functions that are only called by the quiz module itself are in {@link locallib.php}
22  *
23  * @package    mod
24  * @subpackage quiz
25  * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
26  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27  */
30 defined('MOODLE_INTERNAL') || die();
32 require_once($CFG->libdir . '/eventslib.php');
33 require_once($CFG->dirroot . '/calendar/lib.php');
36 /**#@+
37  * Option controlling what options are offered on the quiz settings form.
38  */
39 define('QUIZ_MAX_ATTEMPT_OPTION', 10);
40 define('QUIZ_MAX_QPP_OPTION', 50);
41 define('QUIZ_MAX_DECIMAL_OPTION', 5);
42 define('QUIZ_MAX_Q_DECIMAL_OPTION', 7);
43 /**#@-*/
45 /**#@+
46  * Options determining how the grades from individual attempts are combined to give
47  * the overall grade for a user
48  */
49 define('QUIZ_GRADEHIGHEST', '1');
50 define('QUIZ_GRADEAVERAGE', '2');
51 define('QUIZ_ATTEMPTFIRST', '3');
52 define('QUIZ_ATTEMPTLAST',  '4');
53 /**#@-*/
55 /**
56  * @var int If start and end date for the quiz are more than this many seconds apart
57  * they will be represented by two separate events in the calendar
58  */
59 define('QUIZ_MAX_EVENT_LENGTH', 5*24*60*60); // 5 days.
61 /**#@+
62  * Options for navigation method within quizzes.
63  */
64 define('QUIZ_NAVMETHOD_FREE', 'free');
65 define('QUIZ_NAVMETHOD_SEQ',  'sequential');
66 /**#@-*/
68 /**
69  * Given an object containing all the necessary data,
70  * (defined by the form in mod_form.php) this function
71  * will create a new instance and return the id number
72  * of the new instance.
73  *
74  * @param object $quiz the data that came from the form.
75  * @return mixed the id of the new instance on success,
76  *          false or a string error message on failure.
77  */
78 function quiz_add_instance($quiz) {
79     global $DB;
80     $cmid = $quiz->coursemodule;
82     // Process the options from the form.
83     $quiz->created = time();
84     $quiz->questions = '';
85     $result = quiz_process_options($quiz);
86     if ($result && is_string($result)) {
87         return $result;
88     }
90     // Try to store it in the database.
91     $quiz->id = $DB->insert_record('quiz', $quiz);
93     // Do the processing required after an add or an update.
94     quiz_after_add_or_update($quiz);
96     return $quiz->id;
97 }
99 /**
100  * Given an object containing all the necessary data,
101  * (defined by the form in mod_form.php) this function
102  * will update an existing instance with new data.
103  *
104  * @param object $quiz the data that came from the form.
105  * @return mixed true on success, false or a string error message on failure.
106  */
107 function quiz_update_instance($quiz, $mform) {
108     global $CFG, $DB;
110     // Process the options from the form.
111     $result = quiz_process_options($quiz);
112     if ($result && is_string($result)) {
113         return $result;
114     }
116     $oldquiz = $DB->get_record('quiz', array('id' => $quiz->instance));
118     // Repaginate, if asked to.
119     if (!$quiz->shufflequestions && !empty($quiz->repaginatenow)) {
120         require_once($CFG->dirroot . '/mod/quiz/locallib.php');
121         $quiz->questions = quiz_repaginate(quiz_clean_layout($oldquiz->questions, true),
122                 $quiz->questionsperpage);
123     }
124     unset($quiz->repaginatenow);
126     // Update the database.
127     $quiz->id = $quiz->instance;
128     $DB->update_record('quiz', $quiz);
130     // Do the processing required after an add or an update.
131     quiz_after_add_or_update($quiz);
133     if ($oldquiz->grademethod != $quiz->grademethod) {
134         require_once($CFG->dirroot . '/mod/quiz/locallib.php');
135         $quiz->sumgrades = $oldquiz->sumgrades;
136         $quiz->grade = $oldquiz->grade;
137         quiz_update_all_final_grades($quiz);
138         quiz_update_grades($quiz);
139     }
141     // Delete any previous preview attempts.
142     quiz_delete_previews($quiz);
144     return true;
147 /**
148  * Given an ID of an instance of this module,
149  * this function will permanently delete the instance
150  * and any data that depends on it.
151  *
152  * @param int $id the id of the quiz to delete.
153  * @return bool success or failure.
154  */
155 function quiz_delete_instance($id) {
156     global $DB;
158     $quiz = $DB->get_record('quiz', array('id' => $id), '*', MUST_EXIST);
160     quiz_delete_all_attempts($quiz);
161     quiz_delete_all_overrides($quiz);
163     $DB->delete_records('quiz_question_instances', array('quiz' => $quiz->id));
164     $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
166     $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id));
167     foreach ($events as $event) {
168         $event = calendar_event::load($event);
169         $event->delete();
170     }
172     quiz_grade_item_delete($quiz);
173     $DB->delete_records('quiz', array('id' => $quiz->id));
175     return true;
178 /**
179  * Deletes a quiz override from the database and clears any corresponding calendar events
180  *
181  * @param object $quiz The quiz object.
182  * @param int $overrideid The id of the override being deleted
183  * @return bool true on success
184  */
185 function quiz_delete_override($quiz, $overrideid) {
186     global $DB;
188     $override = $DB->get_record('quiz_overrides', array('id' => $overrideid), '*', MUST_EXIST);
190     // Delete the events.
191     $events = $DB->get_records('event', array('modulename' => 'quiz',
192             'instance' => $quiz->id, 'groupid' => (int)$override->groupid,
193             'userid' => (int)$override->userid));
194     foreach ($events as $event) {
195         $eventold = calendar_event::load($event);
196         $eventold->delete();
197     }
199     $DB->delete_records('quiz_overrides', array('id' => $overrideid));
200     return true;
203 /**
204  * Deletes all quiz overrides from the database and clears any corresponding calendar events
205  *
206  * @param object $quiz The quiz object.
207  */
208 function quiz_delete_all_overrides($quiz) {
209     global $DB;
211     $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id');
212     foreach ($overrides as $override) {
213         quiz_delete_override($quiz, $override->id);
214     }
217 /**
218  * Updates a quiz object with override information for a user.
219  *
220  * Algorithm:  For each quiz setting, if there is a matching user-specific override,
221  *   then use that otherwise, if there are group-specific overrides, return the most
222  *   lenient combination of them.  If neither applies, leave the quiz setting unchanged.
223  *
224  *   Special case: if there is more than one password that applies to the user, then
225  *   quiz->extrapasswords will contain an array of strings giving the remaining
226  *   passwords.
227  *
228  * @param object $quiz The quiz object.
229  * @param int $userid The userid.
230  * @return object $quiz The updated quiz object.
231  */
232 function quiz_update_effective_access($quiz, $userid) {
233     global $DB;
235     // Check for user override.
236     $override = $DB->get_record('quiz_overrides', array('quiz' => $quiz->id, 'userid' => $userid));
238     if (!$override) {
239         $override = new stdClass();
240         $override->timeopen = null;
241         $override->timeclose = null;
242         $override->timelimit = null;
243         $override->attempts = null;
244         $override->password = null;
245     }
247     // Check for group overrides.
248     $groupings = groups_get_user_groups($quiz->course, $userid);
250     if (!empty($groupings[0])) {
251         // Select all overrides that apply to the User's groups.
252         list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
253         $sql = "SELECT * FROM {quiz_overrides}
254                 WHERE groupid $extra AND quiz = ?";
255         $params[] = $quiz->id;
256         $records = $DB->get_records_sql($sql, $params);
258         // Combine the overrides.
259         $opens = array();
260         $closes = array();
261         $limits = array();
262         $attempts = array();
263         $passwords = array();
265         foreach ($records as $gpoverride) {
266             if (isset($gpoverride->timeopen)) {
267                 $opens[] = $gpoverride->timeopen;
268             }
269             if (isset($gpoverride->timeclose)) {
270                 $closes[] = $gpoverride->timeclose;
271             }
272             if (isset($gpoverride->timelimit)) {
273                 $limits[] = $gpoverride->timelimit;
274             }
275             if (isset($gpoverride->attempts)) {
276                 $attempts[] = $gpoverride->attempts;
277             }
278             if (isset($gpoverride->password)) {
279                 $passwords[] = $gpoverride->password;
280             }
281         }
282         // If there is a user override for a setting, ignore the group override.
283         if (is_null($override->timeopen) && count($opens)) {
284             $override->timeopen = min($opens);
285         }
286         if (is_null($override->timeclose) && count($closes)) {
287             $override->timeclose = max($closes);
288         }
289         if (is_null($override->timelimit) && count($limits)) {
290             $override->timelimit = max($limits);
291         }
292         if (is_null($override->attempts) && count($attempts)) {
293             $override->attempts = max($attempts);
294         }
295         if (is_null($override->password) && count($passwords)) {
296             $override->password = array_shift($passwords);
297             if (count($passwords)) {
298                 $override->extrapasswords = $passwords;
299             }
300         }
302     }
304     // Merge with quiz defaults.
305     $keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password', 'extrapasswords');
306     foreach ($keys as $key) {
307         if (isset($override->{$key})) {
308             $quiz->{$key} = $override->{$key};
309         }
310     }
312     return $quiz;
315 /**
316  * Delete all the attempts belonging to a quiz.
317  *
318  * @param object $quiz The quiz object.
319  */
320 function quiz_delete_all_attempts($quiz) {
321     global $CFG, $DB;
322     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
323     question_engine::delete_questions_usage_by_activities(new qubaids_for_quiz($quiz->id));
324     $DB->delete_records('quiz_attempts', array('quiz' => $quiz->id));
325     $DB->delete_records('quiz_grades', array('quiz' => $quiz->id));
328 /**
329  * Get the best current grade for a particular user in a quiz.
330  *
331  * @param object $quiz the quiz settings.
332  * @param int $userid the id of the user.
333  * @return float the user's current grade for this quiz, or null if this user does
334  * not have a grade on this quiz.
335  */
336 function quiz_get_best_grade($quiz, $userid) {
337     global $DB;
338     $grade = $DB->get_field('quiz_grades', 'grade',
339             array('quiz' => $quiz->id, 'userid' => $userid));
341     // Need to detect errors/no result, without catching 0 grades.
342     if ($grade === false) {
343         return null;
344     }
346     return $grade + 0; // Convert to number.
349 /**
350  * Is this a graded quiz? If this method returns true, you can assume that
351  * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to
352  * divide by them).
353  *
354  * @param object $quiz a row from the quiz table.
355  * @return bool whether this is a graded quiz.
356  */
357 function quiz_has_grades($quiz) {
358     return $quiz->grade >= 0.000005 && $quiz->sumgrades >= 0.000005;
361 /**
362  * Return a small object with summary information about what a
363  * user has done with a given particular instance of this module
364  * Used for user activity reports.
365  * $return->time = the time they did it
366  * $return->info = a short text description
367  *
368  * @param object $course
369  * @param object $user
370  * @param object $mod
371  * @param object $quiz
372  * @return object|null
373  */
374 function quiz_user_outline($course, $user, $mod, $quiz) {
375     global $DB, $CFG;
376     require_once("$CFG->libdir/gradelib.php");
377     $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
379     if (empty($grades->items[0]->grades)) {
380         return null;
381     } else {
382         $grade = reset($grades->items[0]->grades);
383     }
385     $result = new stdClass();
386     $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
388     // Datesubmitted == time created. dategraded == time modified or time overridden
389     // if grade was last modified by the user themselves use date graded. Otherwise use
390     // date submitted.
391     // TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704.
392     if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
393         $result->time = $grade->dategraded;
394     } else {
395         $result->time = $grade->datesubmitted;
396     }
398     return $result;
401 /**
402  * Print a detailed representation of what a  user has done with
403  * a given particular instance of this module, for user activity reports.
404  *
405  * @param object $course
406  * @param object $user
407  * @param object $mod
408  * @param object $quiz
409  * @return bool
410  */
411 function quiz_user_complete($course, $user, $mod, $quiz) {
412     global $DB, $CFG, $OUTPUT;
413     require_once($CFG->libdir . '/gradelib.php');
414     require_once($CFG->libdir . '/mod/quiz/locallib.php');
416     $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
417     if (!empty($grades->items[0]->grades)) {
418         $grade = reset($grades->items[0]->grades);
419         echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
420         if ($grade->str_feedback) {
421             echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
422         }
423     }
425     if ($attempts = $DB->get_records('quiz_attempts',
426             array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) {
427         foreach ($attempts as $attempt) {
428             echo get_string('attempt', 'quiz').' '.$attempt->attempt.': ';
429             if ($attempt->state != quiz_attempt::FINISHED) {
430                 echo quiz_attempt_state_name($attempt->state);
431             } else {
432                 echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' .
433                         quiz_format_grade($quiz, $quiz->sumgrades);
434             }
435             echo ' - '.userdate($attempt->timemodified).'<br />';
436         }
437     } else {
438         print_string('noattempts', 'quiz');
439     }
441     return true;
444 /**
445  * Quiz periodic clean-up tasks.
446  */
447 function quiz_cron() {
448     global $CFG;
450     // Since the quiz specifies $module->cron = 60, so that the subplugins can
451     // have frequent cron if they need it, we now need to do our own scheduling.
452     $quizconfig = get_config('quiz');
453     if (!isset($quizconfig->overduelastrun)) {
454         $quizconfig->overduelastrun = 0;
455         $quizconfig->overduedoneto  = 0;
456     }
458     $timenow = time();
459     if ($timenow > $quizconfig->overduelastrun + 3600) {
460         require_once($CFG->dirroot . '/mod/quiz/cronlib.php');
461         $overduehander = new mod_quiz_overdue_attempt_updater();
463         $processto = $timenow - $quizconfig->graceperiodmin;
464         $overduehander->update_overdue_attempts($timenow, $quizconfig->overduedoneto, $processto);
466         set_config('overduelastrun', $timenow, 'quiz');
467         set_config('overduedoneto', $processto, 'quiz');
468     }
470     // Run cron for our sub-plugin types.
471     cron_execute_plugin_type('quiz', 'quiz reports');
472     cron_execute_plugin_type('quizaccess', 'quiz access rules');
474     return true;
477 /**
478  * @param int $quizid the quiz id.
479  * @param int $userid the userid.
480  * @param string $status 'all', 'finished' or 'unfinished' to control
481  * @param bool $includepreviews
482  * @return an array of all the user's attempts at this quiz. Returns an empty
483  *      array if there are none.
484  */
485 function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
486     global $DB, $CFG;
487     // TODO MDL-33071 it is very annoying to have to included all of locallib.php
488     // just to get the quiz_attempt::FINISHED constants, but I will try to sort
489     // that out properly for Moodle 2.4. For now, I will just do a quick fix for
490     // MDL-33048.
491     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
493     $params = array();
494     switch ($status) {
495         case 'all':
496             $statuscondition = '';
497             break;
499         case 'finished':
500             $statuscondition = ' AND state IN (:state1, :state2)';
501             $params['state1'] = quiz_attempt::FINISHED;
502             $params['state2'] = quiz_attempt::ABANDONED;
503             break;
505         case 'unfinished':
506             $statuscondition = ' AND state IN (:state1, :state2)';
507             $params['state1'] = quiz_attempt::IN_PROGRESS;
508             $params['state2'] = quiz_attempt::OVERDUE;
509             break;
510     }
512     $previewclause = '';
513     if (!$includepreviews) {
514         $previewclause = ' AND preview = 0';
515     }
517     $params['quizid'] = $quizid;
518     $params['userid'] = $userid;
519     return $DB->get_records_select('quiz_attempts',
520             'quiz = :quizid AND userid = :userid' . $previewclause . $statuscondition,
521             $params, 'attempt ASC');
524 /**
525  * Return grade for given user or all users.
526  *
527  * @param int $quizid id of quiz
528  * @param int $userid optional user id, 0 means all users
529  * @return array array of grades, false if none. These are raw grades. They should
530  * be processed with quiz_format_grade for display.
531  */
532 function quiz_get_user_grades($quiz, $userid = 0) {
533     global $CFG, $DB;
535     $params = array($quiz->id);
536     $usertest = '';
537     if ($userid) {
538         $params[] = $userid;
539         $usertest = 'AND u.id = ?';
540     }
541     return $DB->get_records_sql("
542             SELECT
543                 u.id,
544                 u.id AS userid,
545                 qg.grade AS rawgrade,
546                 qg.timemodified AS dategraded,
547                 MAX(qa.timefinish) AS datesubmitted
549             FROM {user} u
550             JOIN {quiz_grades} qg ON u.id = qg.userid
551             JOIN {quiz_attempts} qa ON qa.quiz = qg.quiz AND qa.userid = u.id
553             WHERE qg.quiz = ?
554             $usertest
555             GROUP BY u.id, qg.grade, qg.timemodified", $params);
558 /**
559  * Round a grade to to the correct number of decimal places, and format it for display.
560  *
561  * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
562  * @param float $grade The grade to round.
563  * @return float
564  */
565 function quiz_format_grade($quiz, $grade) {
566     if (is_null($grade)) {
567         return get_string('notyetgraded', 'quiz');
568     }
569     return format_float($grade, $quiz->decimalpoints);
572 /**
573  * Round a grade to to the correct number of decimal places, and format it for display.
574  *
575  * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
576  * @param float $grade The grade to round.
577  * @return float
578  */
579 function quiz_format_question_grade($quiz, $grade) {
580     if (empty($quiz->questiondecimalpoints)) {
581         $quiz->questiondecimalpoints = -1;
582     }
583     if ($quiz->questiondecimalpoints == -1) {
584         return format_float($grade, $quiz->decimalpoints);
585     } else {
586         return format_float($grade, $quiz->questiondecimalpoints);
587     }
590 /**
591  * Update grades in central gradebook
592  *
593  * @category grade
594  * @param object $quiz the quiz settings.
595  * @param int $userid specific user only, 0 means all users.
596  * @param bool $nullifnone If a single user is specified and $nullifnone is true a grade item with a null rawgrade will be inserted
597  */
598 function quiz_update_grades($quiz, $userid = 0, $nullifnone = true) {
599     global $CFG, $DB;
600     require_once($CFG->libdir.'/gradelib.php');
602     if ($quiz->grade == 0) {
603         quiz_grade_item_update($quiz);
605     } else if ($grades = quiz_get_user_grades($quiz, $userid)) {
606         quiz_grade_item_update($quiz, $grades);
608     } else if ($userid && $nullifnone) {
609         $grade = new stdClass();
610         $grade->userid = $userid;
611         $grade->rawgrade = null;
612         quiz_grade_item_update($quiz, $grade);
614     } else {
615         quiz_grade_item_update($quiz);
616     }
619 /**
620  * Update all grades in gradebook.
621  */
622 function quiz_upgrade_grades() {
623     global $DB;
625     $sql = "SELECT COUNT('x')
626               FROM {quiz} a, {course_modules} cm, {modules} m
627              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
628     $count = $DB->count_records_sql($sql);
630     $sql = "SELECT a.*, cm.idnumber AS cmidnumber, a.course AS courseid
631               FROM {quiz} a, {course_modules} cm, {modules} m
632              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
633     $rs = $DB->get_recordset_sql($sql);
634     if ($rs->valid()) {
635         $pbar = new progress_bar('quizupgradegrades', 500, true);
636         $i=0;
637         foreach ($rs as $quiz) {
638             $i++;
639             upgrade_set_timeout(60*5); // Set up timeout, may also abort execution.
640             quiz_update_grades($quiz, 0, false);
641             $pbar->update($i, $count, "Updating Quiz grades ($i/$count).");
642         }
643     }
644     $rs->close();
647 /**
648  * Create or update the grade item for given quiz
649  *
650  * @category grade
651  * @param object $quiz object with extra cmidnumber
652  * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
653  * @return int 0 if ok, error code otherwise
654  */
655 function quiz_grade_item_update($quiz, $grades = null) {
656     global $CFG, $OUTPUT;
657     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
658     require_once($CFG->libdir.'/gradelib.php');
660     if (array_key_exists('cmidnumber', $quiz)) { // May not be always present.
661         $params = array('itemname' => $quiz->name, 'idnumber' => $quiz->cmidnumber);
662     } else {
663         $params = array('itemname' => $quiz->name);
664     }
666     if ($quiz->grade > 0) {
667         $params['gradetype'] = GRADE_TYPE_VALUE;
668         $params['grademax']  = $quiz->grade;
669         $params['grademin']  = 0;
671     } else {
672         $params['gradetype'] = GRADE_TYPE_NONE;
673     }
675     // What this is trying to do:
676     // 1. If the quiz is set to not show grades while the quiz is still open,
677     //    and is set to show grades after the quiz is closed, then create the
678     //    grade_item with a show-after date that is the quiz close date.
679     // 2. If the quiz is set to not show grades at either of those times,
680     //    create the grade_item as hidden.
681     // 3. If the quiz is set to show grades, create the grade_item visible.
682     $openreviewoptions = mod_quiz_display_options::make_from_quiz($quiz,
683             mod_quiz_display_options::LATER_WHILE_OPEN);
684     $closedreviewoptions = mod_quiz_display_options::make_from_quiz($quiz,
685             mod_quiz_display_options::AFTER_CLOSE);
686     if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX &&
687             $closedreviewoptions->marks < question_display_options::MARK_AND_MAX) {
688         $params['hidden'] = 1;
690     } else if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX &&
691             $closedreviewoptions->marks >= question_display_options::MARK_AND_MAX) {
692         if ($quiz->timeclose) {
693             $params['hidden'] = $quiz->timeclose;
694         } else {
695             $params['hidden'] = 1;
696         }
698     } else {
699         // Either
700         // a) both open and closed enabled
701         // b) open enabled, closed disabled - we can not "hide after",
702         //    grades are kept visible even after closing.
703         $params['hidden'] = 0;
704     }
706     if ($grades  === 'reset') {
707         $params['reset'] = true;
708         $grades = null;
709     }
711     $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
712     if (!empty($gradebook_grades->items)) {
713         $grade_item = $gradebook_grades->items[0];
714         if ($grade_item->locked) {
715             // NOTE: this is an extremely nasty hack! It is not a bug if this confirmation fails badly. --skodak.
716             $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
717             if (!$confirm_regrade) {
718                 $message = get_string('gradeitemislocked', 'grades');
719                 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id .
720                         '&amp;mode=overview';
721                 $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
722                 echo $OUTPUT->box_start('generalbox', 'notice');
723                 echo '<p>'. $message .'</p>';
724                 echo $OUTPUT->container_start('buttons');
725                 echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades'));
726                 echo $OUTPUT->single_button($back_link,  get_string('cancel'));
727                 echo $OUTPUT->container_end();
728                 echo $OUTPUT->box_end();
730                 return GRADE_UPDATE_ITEM_LOCKED;
731             }
732         }
733     }
735     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
738 /**
739  * Delete grade item for given quiz
740  *
741  * @category grade
742  * @param object $quiz object
743  * @return object quiz
744  */
745 function quiz_grade_item_delete($quiz) {
746     global $CFG;
747     require_once($CFG->libdir . '/gradelib.php');
749     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0,
750             null, array('deleted' => 1));
753 /**
754  * This standard function will check all instances of this module
755  * and make sure there are up-to-date events created for each of them.
756  * If courseid = 0, then every quiz event in the site is checked, else
757  * only quiz events belonging to the course specified are checked.
758  * This function is used, in its new format, by restore_refresh_events()
759  *
760  * @param int $courseid
761  * @return bool
762  */
763 function quiz_refresh_events($courseid = 0) {
764     global $DB;
766     if ($courseid == 0) {
767         if (!$quizzes = $DB->get_records('quiz')) {
768             return true;
769         }
770     } else {
771         if (!$quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
772             return true;
773         }
774     }
776     foreach ($quizzes as $quiz) {
777         quiz_update_events($quiz);
778     }
780     return true;
783 /**
784  * Returns all quiz graded users since a given time for specified quiz
785  */
786 function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
787         $courseid, $cmid, $userid = 0, $groupid = 0) {
788     global $CFG, $COURSE, $USER, $DB;
789     require_once('locallib.php');
791     if ($COURSE->id == $courseid) {
792         $course = $COURSE;
793     } else {
794         $course = $DB->get_record('course', array('id' => $courseid));
795     }
797     $modinfo = get_fast_modinfo($course);
799     $cm = $modinfo->cms[$cmid];
800     $quiz = $DB->get_record('quiz', array('id' => $cm->instance));
802     if ($userid) {
803         $userselect = "AND u.id = :userid";
804         $params['userid'] = $userid;
805     } else {
806         $userselect = '';
807     }
809     if ($groupid) {
810         $groupselect = 'AND gm.groupid = :groupid';
811         $groupjoin   = 'JOIN {groups_members} gm ON  gm.userid=u.id';
812         $params['groupid'] = $groupid;
813     } else {
814         $groupselect = '';
815         $groupjoin   = '';
816     }
818     $params['timestart'] = $timestart;
819     $params['quizid'] = $quiz->id;
821     if (!$attempts = $DB->get_records_sql("
822               SELECT qa.*,
823                      u.firstname, u.lastname, u.email, u.picture, u.imagealt
824                 FROM {quiz_attempts} qa
825                      JOIN {user} u ON u.id = qa.userid
826                      $groupjoin
827                WHERE qa.timefinish > :timestart
828                  AND qa.quiz = :quizid
829                  AND qa.preview = 0
830                      $userselect
831                      $groupselect
832             ORDER BY qa.timefinish ASC", $params)) {
833         return;
834     }
836     $context         = get_context_instance(CONTEXT_MODULE, $cm->id);
837     $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
838     $viewfullnames   = has_capability('moodle/site:viewfullnames', $context);
839     $grader          = has_capability('mod/quiz:viewreports', $context);
840     $groupmode       = groups_get_activity_groupmode($cm, $course);
842     if (is_null($modinfo->groups)) {
843         // Load all my groups and cache it in modinfo.
844         $modinfo->groups = groups_get_user_groups($course->id);
845     }
847     $usersgroups = null;
848     $aname = format_string($cm->name, true);
849     foreach ($attempts as $attempt) {
850         if ($attempt->userid != $USER->id) {
851             if (!$grader) {
852                 // Grade permission required.
853                 continue;
854             }
856             if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
857                 if (is_null($usersgroups)) {
858                     $usersgroups = groups_get_all_groups($course->id,
859                             $attempt->userid, $cm->groupingid);
860                     if (is_array($usersgroups)) {
861                         $usersgroups = array_keys($usersgroups);
862                     } else {
863                         $usersgroups = array();
864                     }
865                 }
866                 if (!array_intersect($usersgroups, $modinfo->groups[$cm->id])) {
867                     continue;
868                 }
869             }
870         }
872         $options = quiz_get_review_options($quiz, $attempt, $context);
874         $tmpactivity = new stdClass();
876         $tmpactivity->type       = 'quiz';
877         $tmpactivity->cmid       = $cm->id;
878         $tmpactivity->name       = $aname;
879         $tmpactivity->sectionnum = $cm->sectionnum;
880         $tmpactivity->timestamp  = $attempt->timefinish;
882         $tmpactivity->content->attemptid = $attempt->id;
883         $tmpactivity->content->attempt   = $attempt->attempt;
884         if (quiz_has_grades($quiz) && $options->marks >= question_display_options::MARK_AND_MAX) {
885             $tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades);
886             $tmpactivity->content->maxgrade  = quiz_format_grade($quiz, $quiz->sumgrades);
887         } else {
888             $tmpactivity->content->sumgrades = null;
889             $tmpactivity->content->maxgrade  = null;
890         }
892         $tmpactivity->user->id        = $attempt->userid;
893         $tmpactivity->user->firstname = $attempt->firstname;
894         $tmpactivity->user->lastname  = $attempt->lastname;
895         $tmpactivity->user->fullname  = fullname($attempt, $viewfullnames);
896         $tmpactivity->user->picture   = $attempt->picture;
897         $tmpactivity->user->imagealt  = $attempt->imagealt;
898         $tmpactivity->user->email     = $attempt->email;
900         $activities[$index++] = $tmpactivity;
901     }
904 function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
905     global $CFG, $OUTPUT;
907     echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">';
909     echo '<tr><td class="userpicture" valign="top">';
910     echo $OUTPUT->user_picture($activity->user, array('courseid' => $courseid));
911     echo '</td><td>';
913     if ($detail) {
914         $modname = $modnames[$activity->type];
915         echo '<div class="title">';
916         echo '<img src="' . $OUTPUT->pix_url('icon', $activity->type) . '" ' .
917                 'class="icon" alt="' . $modname . '" />';
918         echo '<a href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
919                 $activity->cmid . '">' . $activity->name . '</a>';
920         echo '</div>';
921     }
923     echo '<div class="grade">';
924     echo  get_string('attempt', 'quiz', $activity->content->attempt);
925     if (isset($activity->content->maxgrade)) {
926         $grades = $activity->content->sumgrades . ' / ' . $activity->content->maxgrade;
927         echo ': (<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?attempt=' .
928                 $activity->content->attemptid . '">' . $grades . '</a>)';
929     }
930     echo '</div>';
932     echo '<div class="user">';
933     echo '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $activity->user->id .
934             '&amp;course=' . $courseid . '">' . $activity->user->fullname .
935             '</a> - ' . userdate($activity->timestamp);
936     echo '</div>';
938     echo '</td></tr></table>';
940     return;
943 /**
944  * Pre-process the quiz options form data, making any necessary adjustments.
945  * Called by add/update instance in this file.
946  *
947  * @param object $quiz The variables set on the form.
948  */
949 function quiz_process_options($quiz) {
950     global $CFG;
951     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
952     require_once($CFG->libdir . '/questionlib.php');
954     $quiz->timemodified = time();
956     // Quiz name.
957     if (!empty($quiz->name)) {
958         $quiz->name = trim($quiz->name);
959     }
961     // Password field - different in form to stop browsers that remember passwords
962     // getting confused.
963     $quiz->password = $quiz->quizpassword;
964     unset($quiz->quizpassword);
966     // Quiz feedback.
967     if (isset($quiz->feedbacktext)) {
968         // Clean up the boundary text.
969         for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
970             if (empty($quiz->feedbacktext[$i]['text'])) {
971                 $quiz->feedbacktext[$i]['text'] = '';
972             } else {
973                 $quiz->feedbacktext[$i]['text'] = trim($quiz->feedbacktext[$i]['text']);
974             }
975         }
977         // Check the boundary value is a number or a percentage, and in range.
978         $i = 0;
979         while (!empty($quiz->feedbackboundaries[$i])) {
980             $boundary = trim($quiz->feedbackboundaries[$i]);
981             if (!is_numeric($boundary)) {
982                 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
983                     $boundary = trim(substr($boundary, 0, -1));
984                     if (is_numeric($boundary)) {
985                         $boundary = $boundary * $quiz->grade / 100.0;
986                     } else {
987                         return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
988                     }
989                 }
990             }
991             if ($boundary <= 0 || $boundary >= $quiz->grade) {
992                 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
993             }
994             if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
995                 return get_string('feedbackerrororder', 'quiz', $i + 1);
996             }
997             $quiz->feedbackboundaries[$i] = $boundary;
998             $i += 1;
999         }
1000         $numboundaries = $i;
1002         // Check there is nothing in the remaining unused fields.
1003         if (!empty($quiz->feedbackboundaries)) {
1004             for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
1005                 if (!empty($quiz->feedbackboundaries[$i]) &&
1006                         trim($quiz->feedbackboundaries[$i]) != '') {
1007                     return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
1008                 }
1009             }
1010         }
1011         for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
1012             if (!empty($quiz->feedbacktext[$i]['text']) &&
1013                     trim($quiz->feedbacktext[$i]['text']) != '') {
1014                 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
1015             }
1016         }
1017         // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
1018         $quiz->feedbackboundaries[-1] = $quiz->grade + 1;
1019         $quiz->feedbackboundaries[$numboundaries] = 0;
1020         $quiz->feedbackboundarycount = $numboundaries;
1021     }
1023     // Combing the individual settings into the review columns.
1024     $quiz->reviewattempt = quiz_review_option_form_to_db($quiz, 'attempt');
1025     $quiz->reviewcorrectness = quiz_review_option_form_to_db($quiz, 'correctness');
1026     $quiz->reviewmarks = quiz_review_option_form_to_db($quiz, 'marks');
1027     $quiz->reviewspecificfeedback = quiz_review_option_form_to_db($quiz, 'specificfeedback');
1028     $quiz->reviewgeneralfeedback = quiz_review_option_form_to_db($quiz, 'generalfeedback');
1029     $quiz->reviewrightanswer = quiz_review_option_form_to_db($quiz, 'rightanswer');
1030     $quiz->reviewoverallfeedback = quiz_review_option_form_to_db($quiz, 'overallfeedback');
1031     $quiz->reviewattempt |= mod_quiz_display_options::DURING;
1032     $quiz->reviewoverallfeedback &= ~mod_quiz_display_options::DURING;
1035 /**
1036  * Helper function for {@link quiz_process_options()}.
1037  * @param object $fromform the sumbitted form date.
1038  * @param string $field one of the review option field names.
1039  */
1040 function quiz_review_option_form_to_db($fromform, $field) {
1041     static $times = array(
1042         'during' => mod_quiz_display_options::DURING,
1043         'immediately' => mod_quiz_display_options::IMMEDIATELY_AFTER,
1044         'open' => mod_quiz_display_options::LATER_WHILE_OPEN,
1045         'closed' => mod_quiz_display_options::AFTER_CLOSE,
1046     );
1048     $review = 0;
1049     foreach ($times as $whenname => $when) {
1050         $fieldname = $field . $whenname;
1051         if (isset($fromform->$fieldname)) {
1052             $review |= $when;
1053             unset($fromform->$fieldname);
1054         }
1055     }
1057     return $review;
1060 /**
1061  * This function is called at the end of quiz_add_instance
1062  * and quiz_update_instance, to do the common processing.
1063  *
1064  * @param object $quiz the quiz object.
1065  */
1066 function quiz_after_add_or_update($quiz) {
1067     global $DB;
1068     $cmid = $quiz->coursemodule;
1070     // We need to use context now, so we need to make sure all needed info is already in db.
1071     $DB->set_field('course_modules', 'instance', $quiz->id, array('id'=>$cmid));
1072     $context = get_context_instance(CONTEXT_MODULE, $cmid);
1074     // Save the feedback.
1075     $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
1077     for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) {
1078         $feedback = new stdClass();
1079         $feedback->quizid = $quiz->id;
1080         $feedback->feedbacktext = $quiz->feedbacktext[$i]['text'];
1081         $feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format'];
1082         $feedback->mingrade = $quiz->feedbackboundaries[$i];
1083         $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
1084         $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1085         $feedbacktext = file_save_draft_area_files((int)$quiz->feedbacktext[$i]['itemid'],
1086                 $context->id, 'mod_quiz', 'feedback', $feedback->id,
1087                 array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0),
1088                 $quiz->feedbacktext[$i]['text']);
1089         $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext,
1090                 array('id' => $feedback->id));
1091     }
1093     // Store any settings belonging to the access rules.
1094     quiz_access_manager::save_settings($quiz);
1096     // Update the events relating to this quiz.
1097     quiz_update_events($quiz);
1099     // Update related grade item.
1100     quiz_grade_item_update($quiz);
1103 /**
1104  * This function updates the events associated to the quiz.
1105  * If $override is non-zero, then it updates only the events
1106  * associated with the specified override.
1107  *
1108  * @uses QUIZ_MAX_EVENT_LENGTH
1109  * @param object $quiz the quiz object.
1110  * @param object optional $override limit to a specific override
1111  */
1112 function quiz_update_events($quiz, $override = null) {
1113     global $DB;
1115     // Load the old events relating to this quiz.
1116     $conds = array('modulename'=>'quiz',
1117                    'instance'=>$quiz->id);
1118     if (!empty($override)) {
1119         // Only load events for this override.
1120         $conds['groupid'] = isset($override->groupid)?  $override->groupid : 0;
1121         $conds['userid'] = isset($override->userid)?  $override->userid : 0;
1122     }
1123     $oldevents = $DB->get_records('event', $conds);
1125     // Now make a todo list of all that needs to be updated.
1126     if (empty($override)) {
1127         // We are updating the primary settings for the quiz, so we
1128         // need to add all the overrides.
1129         $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id));
1130         // As well as the original quiz (empty override).
1131         $overrides[] = new stdClass();
1132     } else {
1133         // Just do the one override.
1134         $overrides = array($override);
1135     }
1137     foreach ($overrides as $current) {
1138         $groupid   = isset($current->groupid)?  $current->groupid : 0;
1139         $userid    = isset($current->userid)? $current->userid : 0;
1140         $timeopen  = isset($current->timeopen)?  $current->timeopen : $quiz->timeopen;
1141         $timeclose = isset($current->timeclose)? $current->timeclose : $quiz->timeclose;
1143         // Only add open/close events for an override if they differ from the quiz default.
1144         $addopen  = empty($current->id) || !empty($current->timeopen);
1145         $addclose = empty($current->id) || !empty($current->timeclose);
1147         $event = new stdClass();
1148         $event->description = format_module_intro('quiz', $quiz, $quiz->coursemodule);
1149         // Events module won't show user events when the courseid is nonzero.
1150         $event->courseid    = ($userid) ? 0 : $quiz->course;
1151         $event->groupid     = $groupid;
1152         $event->userid      = $userid;
1153         $event->modulename  = 'quiz';
1154         $event->instance    = $quiz->id;
1155         $event->timestart   = $timeopen;
1156         $event->timeduration = max($timeclose - $timeopen, 0);
1157         $event->visible     = instance_is_visible('quiz', $quiz);
1158         $event->eventtype   = 'open';
1160         // Determine the event name.
1161         if ($groupid) {
1162             $params = new stdClass();
1163             $params->quiz = $quiz->name;
1164             $params->group = groups_get_group_name($groupid);
1165             if ($params->group === false) {
1166                 // Group doesn't exist, just skip it.
1167                 continue;
1168             }
1169             $eventname = get_string('overridegroupeventname', 'quiz', $params);
1170         } else if ($userid) {
1171             $params = new stdClass();
1172             $params->quiz = $quiz->name;
1173             $eventname = get_string('overrideusereventname', 'quiz', $params);
1174         } else {
1175             $eventname = $quiz->name;
1176         }
1177         if ($addopen or $addclose) {
1178             if ($timeclose and $timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) {
1179                 // Single event for the whole quiz.
1180                 if ($oldevent = array_shift($oldevents)) {
1181                     $event->id = $oldevent->id;
1182                 } else {
1183                     unset($event->id);
1184                 }
1185                 $event->name = $eventname;
1186                 // The method calendar_event::create will reuse a db record if the id field is set.
1187                 calendar_event::create($event);
1188             } else {
1189                 // Separate start and end events.
1190                 $event->timeduration  = 0;
1191                 if ($timeopen && $addopen) {
1192                     if ($oldevent = array_shift($oldevents)) {
1193                         $event->id = $oldevent->id;
1194                     } else {
1195                         unset($event->id);
1196                     }
1197                     $event->name = $eventname.' ('.get_string('quizopens', 'quiz').')';
1198                     // The method calendar_event::create will reuse a db record if the id field is set.
1199                     calendar_event::create($event);
1200                 }
1201                 if ($timeclose && $addclose) {
1202                     if ($oldevent = array_shift($oldevents)) {
1203                         $event->id = $oldevent->id;
1204                     } else {
1205                         unset($event->id);
1206                     }
1207                     $event->name      = $eventname.' ('.get_string('quizcloses', 'quiz').')';
1208                     $event->timestart = $timeclose;
1209                     $event->eventtype = 'close';
1210                     calendar_event::create($event);
1211                 }
1212             }
1213         }
1214     }
1216     // Delete any leftover events.
1217     foreach ($oldevents as $badevent) {
1218         $badevent = calendar_event::load($badevent);
1219         $badevent->delete();
1220     }
1223 /**
1224  * @return array
1225  */
1226 function quiz_get_view_actions() {
1227     return array('view', 'view all', 'report', 'review');
1230 /**
1231  * @return array
1232  */
1233 function quiz_get_post_actions() {
1234     return array('attempt', 'close attempt', 'preview', 'editquestions',
1235             'delete attempt', 'manualgrade');
1238 /**
1239  * @param array $questionids of question ids.
1240  * @return bool whether any of these questions are used by any instance of this module.
1241  */
1242 function quiz_questions_in_use($questionids) {
1243     global $DB, $CFG;
1244     require_once($CFG->libdir . '/questionlib.php');
1245     list($test, $params) = $DB->get_in_or_equal($questionids);
1246     return $DB->record_exists_select('quiz_question_instances',
1247             'question ' . $test, $params) || question_engine::questions_in_use(
1248             $questionids, new qubaid_join('{quiz_attempts} quiza',
1249             'quiza.uniqueid', 'quiza.preview = 0'));
1252 /**
1253  * Implementation of the function for printing the form elements that control
1254  * whether the course reset functionality affects the quiz.
1255  *
1256  * @param $mform the course reset form that is being built.
1257  */
1258 function quiz_reset_course_form_definition($mform) {
1259     $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz'));
1260     $mform->addElement('advcheckbox', 'reset_quiz_attempts',
1261             get_string('removeallquizattempts', 'quiz'));
1264 /**
1265  * Course reset form defaults.
1266  * @return array the defaults.
1267  */
1268 function quiz_reset_course_form_defaults($course) {
1269     return array('reset_quiz_attempts' => 1);
1272 /**
1273  * Removes all grades from gradebook
1274  *
1275  * @param int $courseid
1276  * @param string optional type
1277  */
1278 function quiz_reset_gradebook($courseid, $type='') {
1279     global $CFG, $DB;
1281     $quizzes = $DB->get_records_sql("
1282             SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
1283             FROM {modules} m
1284             JOIN {course_modules} cm ON m.id = cm.module
1285             JOIN {quiz} q ON cm.instance = q.id
1286             WHERE m.name = 'quiz' AND cm.course = ?", array($courseid));
1288     foreach ($quizzes as $quiz) {
1289         quiz_grade_item_update($quiz, 'reset');
1290     }
1293 /**
1294  * Actual implementation of the reset course functionality, delete all the
1295  * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is
1296  * set and true.
1297  *
1298  * Also, move the quiz open and close dates, if the course start date is changing.
1299  *
1300  * @param object $data the data submitted from the reset course.
1301  * @return array status array
1302  */
1303 function quiz_reset_userdata($data) {
1304     global $CFG, $DB;
1305     require_once($CFG->libdir.'/questionlib.php');
1307     $componentstr = get_string('modulenameplural', 'quiz');
1308     $status = array();
1310     // Delete attempts.
1311     if (!empty($data->reset_quiz_attempts)) {
1312         require_once($CFG->libdir . '/questionlib.php');
1314         question_engine::delete_questions_usage_by_activities(new qubaid_join(
1315                 '{quiz_attempts} quiza JOIN {quiz} quiz ON quiza.quiz = quiz.id',
1316                 'quiza.uniqueid', 'quiz.course = :quizcourseid',
1317                 array('quizcourseid' => $data->courseid)));
1319         $DB->delete_records_select('quiz_attempts',
1320                 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid));
1321         $status[] = array(
1322             'component' => $componentstr,
1323             'item' => get_string('attemptsdeleted', 'quiz'),
1324             'error' => false);
1326         // Remove all grades from gradebook.
1327         $DB->delete_records_select('quiz_grades',
1328                 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid));
1329         if (empty($data->reset_gradebook_grades)) {
1330             quiz_reset_gradebook($data->courseid);
1331         }
1332         $status[] = array(
1333             'component' => $componentstr,
1334             'item' => get_string('gradesdeleted', 'quiz'),
1335             'error' => false);
1336     }
1338     // Updating dates - shift may be negative too.
1339     if ($data->timeshift) {
1340         shift_course_mod_dates('quiz', array('timeopen', 'timeclose'),
1341                 $data->timeshift, $data->courseid);
1342         $status[] = array(
1343             'component' => $componentstr,
1344             'item' => get_string('openclosedatesupdated', 'quiz'),
1345             'error' => false);
1346     }
1348     return $status;
1351 /**
1352  * Checks whether the current user is allowed to view a file uploaded in a quiz.
1353  * Teachers can view any from their courses, students can only view their own.
1354  *
1355  * @param int $attemptuniqueid int attempt id
1356  * @param int $questionid int question id
1357  * @return bool to indicate access granted or denied
1358  */
1359 function quiz_check_file_access($attemptuniqueid, $questionid, $context = null) {
1360     global $USER, $DB, $CFG;
1361     require_once(dirname(__FILE__).'/attemptlib.php');
1362     require_once(dirname(__FILE__).'/locallib.php');
1364     $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $attemptuniqueid));
1365     $attemptobj = quiz_attempt::create($attempt->id);
1367     // Does the question exist?
1368     if (!$question = $DB->get_record('question', array('id' => $questionid))) {
1369         return false;
1370     }
1372     if ($context === null) {
1373         $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz));
1374         $cm = get_coursemodule_from_id('quiz', $quiz->id);
1375         $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1376     }
1378     // Load those questions and the associated states.
1379     $attemptobj->load_questions(array($questionid));
1380     $attemptobj->load_question_states(array($questionid));
1382     // Obtain the state.
1383     $state = $attemptobj->get_question_state($questionid);
1384     // Obtain the question.
1385     $question = $attemptobj->get_question($questionid);
1387     // Access granted if the current user submitted this file.
1388     if ($attempt->userid != $USER->id) {
1389         return false;
1390     }
1391     // Access granted if the current user has permission to grade quizzes in this course.
1392     if (!(has_capability('mod/quiz:viewreports', $context) ||
1393             has_capability('mod/quiz:grade', $context))) {
1394         return false;
1395     }
1397     return array($question, $state, array());
1400 /**
1401  * Prints quiz summaries on MyMoodle Page
1402  * @param arry $courses
1403  * @param array $htmlarray
1404  */
1405 function quiz_print_overview($courses, &$htmlarray) {
1406     global $USER, $CFG;
1407     // These next 6 Lines are constant in all modules (just change module name).
1408     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1409         return array();
1410     }
1412     if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
1413         return;
1414     }
1416     // Fetch some language strings outside the main loop.
1417     $strquiz = get_string('modulename', 'quiz');
1418     $strnoattempts = get_string('noattempts', 'quiz');
1420     // We want to list quizzes that are currently available, and which have a close date.
1421     // This is the same as what the lesson does, and the dabate is in MDL-10568.
1422     $now = time();
1423     foreach ($quizzes as $quiz) {
1424         if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
1425             // Give a link to the quiz, and the deadline.
1426             $str = '<div class="quiz overview">' .
1427                     '<div class="name">' . $strquiz . ': <a ' .
1428                     ($quiz->visible ? '' : ' class="dimmed"') .
1429                     ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
1430                     $quiz->coursemodule . '">' .
1431                     $quiz->name . '</a></div>';
1432             $str .= '<div class="info">' . get_string('quizcloseson', 'quiz',
1433                     userdate($quiz->timeclose)) . '</div>';
1435             // Now provide more information depending on the uers's role.
1436             $context = get_context_instance(CONTEXT_MODULE, $quiz->coursemodule);
1437             if (has_capability('mod/quiz:viewreports', $context)) {
1438                 // For teacher-like people, show a summary of the number of student attempts.
1439                 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
1440                 // fields set to make the following call work.
1441                 $str .= '<div class="info">' .
1442                         quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
1443             } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
1444                     $context)) { // Student
1445                 // For student-like people, tell them how many attempts they have made.
1446                 if (isset($USER->id) &&
1447                         ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
1448                     $numattempts = count($attempts);
1449                     $str .= '<div class="info">' .
1450                             get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
1451                 } else {
1452                     $str .= '<div class="info">' . $strnoattempts . '</div>';
1453                 }
1454             } else {
1455                 // For ayone else, there is no point listing this quiz, so stop processing.
1456                 continue;
1457             }
1459             // Add the output for this quiz to the rest.
1460             $str .= '</div>';
1461             if (empty($htmlarray[$quiz->course]['quiz'])) {
1462                 $htmlarray[$quiz->course]['quiz'] = $str;
1463             } else {
1464                 $htmlarray[$quiz->course]['quiz'] .= $str;
1465             }
1466         }
1467     }
1470 /**
1471  * Return a textual summary of the number of attempts that have been made at a particular quiz,
1472  * returns '' if no attempts have been made yet, unless $returnzero is passed as true.
1473  *
1474  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1475  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and
1476  *      $cm->groupingid fields are used at the moment.
1477  * @param bool $returnzero if false (default), when no attempts have been
1478  *      made '' is returned instead of 'Attempts: 0'.
1479  * @param int $currentgroup if there is a concept of current group where this method is being called
1480  *         (e.g. a report) pass it in here. Default 0 which means no current group.
1481  * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
1482  *          "Attemtps 123 (45 from this group)".
1483  */
1484 function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
1485     global $DB, $USER;
1486     $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0));
1487     if ($numattempts || $returnzero) {
1488         if (groups_get_activity_groupmode($cm)) {
1489             $a = new stdClass();
1490             $a->total = $numattempts;
1491             if ($currentgroup) {
1492                 $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' .
1493                         '{quiz_attempts} qa JOIN ' .
1494                         '{groups_members} gm ON qa.userid = gm.userid ' .
1495                         'WHERE quiz = ? AND preview = 0 AND groupid = ?',
1496                         array($quiz->id, $currentgroup));
1497                 return get_string('attemptsnumthisgroup', 'quiz', $a);
1498             } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
1499                 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups));
1500                 $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' .
1501                         '{quiz_attempts} qa JOIN ' .
1502                         '{groups_members} gm ON qa.userid = gm.userid ' .
1503                         'WHERE quiz = ? AND preview = 0 AND ' .
1504                         "groupid $usql", array_merge(array($quiz->id), $params));
1505                 return get_string('attemptsnumyourgroups', 'quiz', $a);
1506             }
1507         }
1508         return get_string('attemptsnum', 'quiz', $numattempts);
1509     }
1510     return '';
1513 /**
1514  * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
1515  * to the quiz reports.
1516  *
1517  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1518  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and
1519  *      $cm->groupingid fields are used at the moment.
1520  * @param object $context the quiz context.
1521  * @param bool $returnzero if false (default), when no attempts have been made
1522  *      '' is returned instead of 'Attempts: 0'.
1523  * @param int $currentgroup if there is a concept of current group where this method is being called
1524  *         (e.g. a report) pass it in here. Default 0 which means no current group.
1525  * @return string HTML fragment for the link.
1526  */
1527 function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false,
1528         $currentgroup = 0) {
1529     global $CFG;
1530     $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
1531     if (!$summary) {
1532         return '';
1533     }
1535     require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1536     $url = new moodle_url('/mod/quiz/report.php', array(
1537             'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
1538     return html_writer::link($url, $summary);
1541 /**
1542  * @param string $feature FEATURE_xx constant for requested feature
1543  * @return bool True if quiz supports feature
1544  */
1545 function quiz_supports($feature) {
1546     switch($feature) {
1547         case FEATURE_GROUPS:                  return true;
1548         case FEATURE_GROUPINGS:               return true;
1549         case FEATURE_GROUPMEMBERSONLY:        return true;
1550         case FEATURE_MOD_INTRO:               return true;
1551         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
1552         case FEATURE_GRADE_HAS_GRADE:         return true;
1553         case FEATURE_GRADE_OUTCOMES:          return false;
1554         case FEATURE_BACKUP_MOODLE2:          return true;
1555         case FEATURE_SHOW_DESCRIPTION:        return true;
1557         default: return null;
1558     }
1561 /**
1562  * @return array all other caps used in module
1563  */
1564 function quiz_get_extra_capabilities() {
1565     global $CFG;
1566     require_once($CFG->libdir.'/questionlib.php');
1567     $caps = question_get_all_capabilities();
1568     $caps[] = 'moodle/site:accessallgroups';
1569     return $caps;
1572 /**
1573  * This fucntion extends the global navigation for the site.
1574  * It is important to note that you should not rely on PAGE objects within this
1575  * body of code as there is no guarantee that during an AJAX request they are
1576  * available
1577  *
1578  * @param navigation_node $quiznode The quiz node within the global navigation
1579  * @param object $course The course object returned from the DB
1580  * @param object $module The module object returned from the DB
1581  * @param object $cm The course module instance returned from the DB
1582  */
1583 function quiz_extend_navigation($quiznode, $course, $module, $cm) {
1584     global $CFG;
1586     $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1588     if (has_capability('mod/quiz:view', $context)) {
1589         $url = new moodle_url('/mod/quiz/view.php', array('id'=>$cm->id));
1590         $quiznode->add(get_string('info', 'quiz'), $url, navigation_node::TYPE_SETTING,
1591                 null, null, new pix_icon('i/info', ''));
1592     }
1594     if (has_any_capability(array('mod/quiz:viewreports', 'mod/quiz:grade'), $context)) {
1595         require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php');
1596         $reportlist = quiz_report_list($context);
1598         $url = new moodle_url('/mod/quiz/report.php',
1599                 array('id' => $cm->id, 'mode' => reset($reportlist)));
1600         $reportnode = $quiznode->add(get_string('results', 'quiz'), $url,
1601                 navigation_node::TYPE_SETTING,
1602                 null, null, new pix_icon('i/report', ''));
1604         foreach ($reportlist as $report) {
1605             $url = new moodle_url('/mod/quiz/report.php',
1606                     array('id' => $cm->id, 'mode' => $report));
1607             $reportnode->add(get_string($report, 'quiz_'.$report), $url,
1608                     navigation_node::TYPE_SETTING,
1609                     null, 'quiz_report_' . $report, new pix_icon('i/item', ''));
1610         }
1611     }
1614 /**
1615  * This function extends the settings navigation block for the site.
1616  *
1617  * It is safe to rely on PAGE here as we will only ever be within the module
1618  * context when this is called
1619  *
1620  * @param settings_navigation $settings
1621  * @param navigation_node $quiznode
1622  */
1623 function quiz_extend_settings_navigation($settings, $quiznode) {
1624     global $PAGE, $CFG;
1626     // Require {@link questionlib.php}
1627     // Included here as we only ever want to include this file if we really need to.
1628     require_once($CFG->libdir . '/questionlib.php');
1630     // We want to add these new nodes after the Edit settings node, and before the
1631     // Locally assigned roles node. Of course, both of those are controlled by capabilities.
1632     $keys = $quiznode->get_children_key_list();
1633     $beforekey = null;
1634     $i = array_search('modedit', $keys);
1635     if ($i === false and array_key_exists(0, $keys)) {
1636         $beforekey = $keys[0];
1637     } else if (array_key_exists($i + 1, $keys)) {
1638         $beforekey = $keys[$i + 1];
1639     }
1641     if (has_capability('mod/quiz:manageoverrides', $PAGE->cm->context)) {
1642         $url = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$PAGE->cm->id));
1643         $node = navigation_node::create(get_string('groupoverrides', 'quiz'),
1644                 new moodle_url($url, array('mode'=>'group')),
1645                 navigation_node::TYPE_SETTING, null, 'mod_quiz_groupoverrides');
1646         $quiznode->add_node($node, $beforekey);
1648         $node = navigation_node::create(get_string('useroverrides', 'quiz'),
1649                 new moodle_url($url, array('mode'=>'user')),
1650                 navigation_node::TYPE_SETTING, null, 'mod_quiz_useroverrides');
1651         $quiznode->add_node($node, $beforekey);
1652     }
1654     if (has_capability('mod/quiz:manage', $PAGE->cm->context)) {
1655         $node = navigation_node::create(get_string('editquiz', 'quiz'),
1656                 new moodle_url('/mod/quiz/edit.php', array('cmid'=>$PAGE->cm->id)),
1657                 navigation_node::TYPE_SETTING, null, 'mod_quiz_edit',
1658                 new pix_icon('t/edit', ''));
1659         $quiznode->add_node($node, $beforekey);
1660     }
1662     if (has_capability('mod/quiz:preview', $PAGE->cm->context)) {
1663         $url = new moodle_url('/mod/quiz/startattempt.php',
1664                 array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey()));
1665         $node = navigation_node::create(get_string('preview', 'quiz'), $url,
1666                 navigation_node::TYPE_SETTING, null, 'mod_quiz_preview',
1667                 new pix_icon('t/preview', ''));
1668         $quiznode->add_node($node, $beforekey);
1669     }
1671     question_extend_settings_navigation($quiznode, $PAGE->cm->context)->trim_if_empty();
1674 /**
1675  * Serves the quiz files.
1676  *
1677  * @package  mod_quiz
1678  * @category files
1679  * @param stdClass $course course object
1680  * @param stdClass $cm course module object
1681  * @param stdClass $context context object
1682  * @param string $filearea file area
1683  * @param array $args extra arguments
1684  * @param bool $forcedownload whether or not force download
1685  * @param array $options additional options affecting the file serving
1686  * @return bool false if file not found, does not return if found - justsend the file
1687  */
1688 function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
1689     global $CFG, $DB;
1691     if ($context->contextlevel != CONTEXT_MODULE) {
1692         return false;
1693     }
1695     require_login($course, false, $cm);
1697     if (!$quiz = $DB->get_record('quiz', array('id'=>$cm->instance))) {
1698         return false;
1699     }
1701     // The 'intro' area is served by pluginfile.php.
1702     $fileareas = array('feedback');
1703     if (!in_array($filearea, $fileareas)) {
1704         return false;
1705     }
1707     $feedbackid = (int)array_shift($args);
1708     if (!$feedback = $DB->get_record('quiz_feedback', array('id'=>$feedbackid))) {
1709         return false;
1710     }
1712     $fs = get_file_storage();
1713     $relativepath = implode('/', $args);
1714     $fullpath = "/$context->id/mod_quiz/$filearea/$feedbackid/$relativepath";
1715     if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1716         return false;
1717     }
1718     send_stored_file($file, 0, 0, true, $options);
1721 /**
1722  * Called via pluginfile.php -> question_pluginfile to serve files belonging to
1723  * a question in a question_attempt when that attempt is a quiz attempt.
1724  *
1725  * @package  mod_quiz
1726  * @category files
1727  * @param stdClass $course course settings object
1728  * @param stdClass $context context object
1729  * @param string $component the name of the component we are serving files for.
1730  * @param string $filearea the name of the file area.
1731  * @param int $qubaid the attempt usage id.
1732  * @param int $slot the id of a question in this quiz attempt.
1733  * @param array $args the remaining bits of the file path.
1734  * @param bool $forcedownload whether the user must be forced to download the file.
1735  * @param array $options additional options affecting the file serving
1736  * @return bool false if file not found, does not return if found - justsend the file
1737  */
1738 function mod_quiz_question_pluginfile($course, $context, $component,
1739         $filearea, $qubaid, $slot, $args, $forcedownload, array $options=array()) {
1740     global $CFG;
1741     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
1743     $attemptobj = quiz_attempt::create_from_usage_id($qubaid);
1744     require_login($attemptobj->get_course(), false, $attemptobj->get_cm());
1746     if ($attemptobj->is_own_attempt() && !$attemptobj->is_finished()) {
1747         // In the middle of an attempt.
1748         if (!$attemptobj->is_preview_user()) {
1749             $attemptobj->require_capability('mod/quiz:attempt');
1750         }
1751         $isreviewing = false;
1753     } else {
1754         // Reviewing an attempt.
1755         $attemptobj->check_review_capability();
1756         $isreviewing = true;
1757     }
1759     if (!$attemptobj->check_file_access($slot, $isreviewing, $context->id,
1760             $component, $filearea, $args, $forcedownload)) {
1761         send_file_not_found();
1762     }
1764     $fs = get_file_storage();
1765     $relativepath = implode('/', $args);
1766     $fullpath = "/$context->id/$component/$filearea/$relativepath";
1767     if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1768         send_file_not_found();
1769     }
1771     send_stored_file($file, 0, 0, $forcedownload, $options);
1774 /**
1775  * Return a list of page types
1776  * @param string $pagetype current page type
1777  * @param stdClass $parentcontext Block's parent context
1778  * @param stdClass $currentcontext Current context of block
1779  */
1780 function quiz_page_type_list($pagetype, $parentcontext, $currentcontext) {
1781     $module_pagetype = array(
1782         'mod-quiz-*'=>get_string('page-mod-quiz-x', 'quiz'),
1783         'mod-quiz-edit'=>get_string('page-mod-quiz-edit', 'quiz'));
1784     return $module_pagetype;
1787 /**
1788  * @return the options for quiz navigation.
1789  */
1790 function quiz_get_navigation_options() {
1791     return array(
1792         QUIZ_NAVMETHOD_FREE => get_string('navmethod_free', 'quiz'),
1793         QUIZ_NAVMETHOD_SEQ  => get_string('navmethod_seq', 'quiz')
1794     );