MDL-28593 Missing include breaks editing quiz grade item.
[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  * If start and end date for the quiz are more than this many seconds apart
47  * they will be represented by two separate events in the calendar
48  */
49 define('QUIZ_MAX_EVENT_LENGTH', 5*24*60*60); // 5 days
51 /**
52  * Given an object containing all the necessary data,
53  * (defined by the form in mod_form.php) this function
54  * will create a new instance and return the id number
55  * of the new instance.
56  *
57  * @param object $quiz the data that came from the form.
58  * @return mixed the id of the new instance on success,
59  *          false or a string error message on failure.
60  */
61 function quiz_add_instance($quiz) {
62     global $DB;
63     $cmid = $quiz->coursemodule;
65     // Process the options from the form.
66     $quiz->created = time();
67     $quiz->questions = '';
68     $result = quiz_process_options($quiz);
69     if ($result && is_string($result)) {
70         return $result;
71     }
73     // Try to store it in the database.
74     $quiz->id = $DB->insert_record('quiz', $quiz);
76     // Do the processing required after an add or an update.
77     quiz_after_add_or_update($quiz);
79     return $quiz->id;
80 }
82 /**
83  * Given an object containing all the necessary data,
84  * (defined by the form in mod_form.php) this function
85  * will update an existing instance with new data.
86  *
87  * @param object $quiz the data that came from the form.
88  * @return mixed true on success, false or a string error message on failure.
89  */
90 function quiz_update_instance($quiz, $mform) {
91     global $CFG, $DB;
93     // Process the options from the form.
94     $result = quiz_process_options($quiz);
95     if ($result && is_string($result)) {
96         return $result;
97     }
99     $oldquiz = $DB->get_record('quiz', array('id' => $quiz->instance));
101     // Repaginate, if asked to.
102     if (!$quiz->shufflequestions && !empty($quiz->repaginatenow)) {
103         require_once($CFG->dirroot . '/mod/quiz/locallib.php');
104         $quiz->questions = quiz_repaginate(quiz_clean_layout($oldquiz->questions, true),
105                 $quiz->questionsperpage);
106     }
107     unset($quiz->repaginatenow);
109     // Update the database.
110     $quiz->id = $quiz->instance;
111     $DB->update_record('quiz', $quiz);
113     // Do the processing required after an add or an update.
114     quiz_after_add_or_update($quiz);
116     if ($oldquiz->grademethod != $quiz->grademethod) {
117         require_once($CFG->dirroot . '/mod/quiz/locallib.php');
118         $quiz->sumgrades = $oldquiz->sumgrades;
119         $quiz->grade = $oldquiz->grade;
120         quiz_update_all_final_grades($quiz);
121         quiz_update_grades($quiz);
122     }
124     // Delete any previous preview attempts
125     quiz_delete_previews($quiz);
127     return true;
130 /**
131  * Given an ID of an instance of this module,
132  * this function will permanently delete the instance
133  * and any data that depends on it.
134  *
135  * @param int $id the id of the quiz to delete.
136  * @return bool success or failure.
137  */
138 function quiz_delete_instance($id) {
139     global $DB;
141     $quiz = $DB->get_record('quiz', array('id' => $id), '*', MUST_EXIST);
143     quiz_delete_all_attempts($quiz);
144     quiz_delete_all_overrides($quiz);
146     $DB->delete_records('quiz_question_instances', array('quiz' => $quiz->id));
147     $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
149     $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id));
150     foreach ($events as $event) {
151         $event = calendar_event::load($event);
152         $event->delete();
153     }
155     quiz_grade_item_delete($quiz);
156     $DB->delete_records('quiz', array('id' => $quiz->id));
158     return true;
161 /**
162  * Deletes a quiz override from the database and clears any corresponding calendar events
163  *
164  * @param object $quiz The quiz object.
165  * @param int $overrideid The id of the override being deleted
166  * @return bool true on success
167  */
168 function quiz_delete_override($quiz, $overrideid) {
169     global $DB;
171     $override = $DB->get_record('quiz_overrides', array('id' => $overrideid), '*', MUST_EXIST);
173     // Delete the events
174     $events = $DB->get_records('event', array('modulename' => 'quiz',
175             'instance' => $quiz->id, 'groupid' => (int)$override->groupid,
176             'userid' => (int)$override->userid));
177     foreach ($events as $event) {
178         $eventold = calendar_event::load($event);
179         $eventold->delete();
180     }
182     $DB->delete_records('quiz_overrides', array('id' => $overrideid));
183     return true;
186 /**
187  * Deletes all quiz overrides from the database and clears any corresponding calendar events
188  *
189  * @param object $quiz The quiz object.
190  */
191 function quiz_delete_all_overrides($quiz) {
192     global $DB;
194     $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id');
195     foreach ($overrides as $override) {
196         quiz_delete_override($quiz, $override->id);
197     }
200 /**
201  * Updates a quiz object with override information for a user.
202  *
203  * Algorithm:  For each quiz setting, if there is a matching user-specific override,
204  *   then use that otherwise, if there are group-specific overrides, return the most
205  *   lenient combination of them.  If neither applies, leave the quiz setting unchanged.
206  *
207  *   Special case: if there is more than one password that applies to the user, then
208  *   quiz->extrapasswords will contain an array of strings giving the remaining
209  *   passwords.
210  *
211  * @param object $quiz The quiz object.
212  * @param int $userid The userid.
213  * @return object $quiz The updated quiz object.
214  */
215 function quiz_update_effective_access($quiz, $userid) {
216     global $DB;
218     // check for user override
219     $override = $DB->get_record('quiz_overrides', array('quiz' => $quiz->id, 'userid' => $userid));
221     if (!$override) {
222         $override = new stdClass();
223         $override->timeopen = null;
224         $override->timeclose = null;
225         $override->timelimit = null;
226         $override->attempts = null;
227         $override->password = null;
228     }
230     // check for group overrides
231     $groupings = groups_get_user_groups($quiz->course, $userid);
233     if (!empty($groupings[0])) {
234         // Select all overrides that apply to the User's groups
235         list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
236         $sql = "SELECT * FROM {quiz_overrides}
237                 WHERE groupid $extra AND quiz = ?";
238         $params[] = $quiz->id;
239         $records = $DB->get_records_sql($sql, $params);
241         // Combine the overrides
242         $opens = array();
243         $closes = array();
244         $limits = array();
245         $attempts = array();
246         $passwords = array();
248         foreach ($records as $gpoverride) {
249             if (isset($gpoverride->timeopen)) {
250                 $opens[] = $gpoverride->timeopen;
251             }
252             if (isset($gpoverride->timeclose)) {
253                 $closes[] = $gpoverride->timeclose;
254             }
255             if (isset($gpoverride->timelimit)) {
256                 $limits[] = $gpoverride->timelimit;
257             }
258             if (isset($gpoverride->attempts)) {
259                 $attempts[] = $gpoverride->attempts;
260             }
261             if (isset($gpoverride->password)) {
262                 $passwords[] = $gpoverride->password;
263             }
264         }
265         // If there is a user override for a setting, ignore the group override
266         if (is_null($override->timeopen) && count($opens)) {
267             $override->timeopen = min($opens);
268         }
269         if (is_null($override->timeclose) && count($closes)) {
270             $override->timeclose = max($closes);
271         }
272         if (is_null($override->timelimit) && count($limits)) {
273             $override->timelimit = max($limits);
274         }
275         if (is_null($override->attempts) && count($attempts)) {
276             $override->attempts = max($attempts);
277         }
278         if (is_null($override->password) && count($passwords)) {
279             $override->password = array_shift($passwords);
280             if (count($passwords)) {
281                 $override->extrapasswords = $passwords;
282             }
283         }
285     }
287     // merge with quiz defaults
288     $keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password', 'extrapasswords');
289     foreach ($keys as $key) {
290         if (isset($override->{$key})) {
291             $quiz->{$key} = $override->{$key};
292         }
293     }
295     return $quiz;
298 /**
299  * Delete all the attempts belonging to a quiz.
300  *
301  * @param object $quiz The quiz object.
302  */
303 function quiz_delete_all_attempts($quiz) {
304     global $CFG, $DB;
305     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
306     question_engine::delete_questions_usage_by_activities(new qubaids_for_quiz($quiz->id));
307     $DB->delete_records('quiz_attempts', array('quiz' => $quiz->id));
308     $DB->delete_records('quiz_grades', array('quiz' => $quiz->id));
311 /**
312  * Get the best current grade for a particular user in a quiz.
313  *
314  * @param object $quiz the quiz settings.
315  * @param int $userid the id of the user.
316  * @return float the user's current grade for this quiz, or null if this user does
317  * not have a grade on this quiz.
318  */
319 function quiz_get_best_grade($quiz, $userid) {
320     global $DB;
321     $grade = $DB->get_field('quiz_grades', 'grade',
322             array('quiz' => $quiz->id, 'userid' => $userid));
324     // Need to detect errors/no result, without catching 0 grades.
325     if ($grade === false) {
326         return null;
327     }
329     return $grade + 0; // Convert to number.
332 /**
333  * Is this a graded quiz? If this method returns true, you can assume that
334  * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to
335  * divide by them).
336  *
337  * @param object $quiz a row from the quiz table.
338  * @return bool whether this is a graded quiz.
339  */
340 function quiz_has_grades($quiz) {
341     return $quiz->grade >= 0.000005 && $quiz->sumgrades >= 0.000005;
344 /**
345  * Return a small object with summary information about what a
346  * user has done with a given particular instance of this module
347  * Used for user activity reports.
348  * $return->time = the time they did it
349  * $return->info = a short text description
350  *
351  * @param object $course
352  * @param object $user
353  * @param object $mod
354  * @param object $quiz
355  * @return object|null
356  */
357 function quiz_user_outline($course, $user, $mod, $quiz) {
358     global $DB, $CFG;
359     require_once("$CFG->libdir/gradelib.php");
360     $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
362     if (empty($grades->items[0]->grades)) {
363         return null;
364     } else {
365         $grade = reset($grades->items[0]->grades);
366     }
368     $result = new stdClass();
369     $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
371     //datesubmitted == time created. dategraded == time modified or time overridden
372     //if grade was last modified by the user themselves use date graded. Otherwise use date submitted
373     //TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
374     if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
375         $result->time = $grade->dategraded;
376     } else {
377         $result->time = $grade->datesubmitted;
378     }
380     return $result;
383 /**
384  * Print a detailed representation of what a  user has done with
385  * a given particular instance of this module, for user activity reports.
386  *
387  * @global object
388  * @param object $course
389  * @param object $user
390  * @param object $mod
391  * @param object $quiz
392  * @return bool
393  */
394 function quiz_user_complete($course, $user, $mod, $quiz) {
395     global $DB, $CFG, $OUTPUT;
396     require_once("$CFG->libdir/gradelib.php");
398     $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
399     if (!empty($grades->items[0]->grades)) {
400         $grade = reset($grades->items[0]->grades);
401         echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
402         if ($grade->str_feedback) {
403             echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
404         }
405     }
407     if ($attempts = $DB->get_records('quiz_attempts',
408             array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) {
409         foreach ($attempts as $attempt) {
410             echo get_string('attempt', 'quiz').' '.$attempt->attempt.': ';
411             if ($attempt->timefinish == 0) {
412                 print_string('unfinished');
413             } else {
414                 echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' .
415                         quiz_format_grade($quiz, $quiz->sumgrades);
416             }
417             echo ' - '.userdate($attempt->timemodified).'<br />';
418         }
419     } else {
420         print_string('noattempts', 'quiz');
421     }
423     return true;
426 /**
427  * Function to be run periodically according to the moodle cron
428  * This function searches for things that need to be done, such
429  * as sending out mail, toggling flags etc ...
430  *
431  * @return bool true
432  */
433 function quiz_cron() {
434     return true;
437 /**
438  * @param int $quizid the quiz id.
439  * @param int $userid the userid.
440  * @param string $status 'all', 'finished' or 'unfinished' to control
441  * @param bool $includepreviews
442  * @return an array of all the user's attempts at this quiz. Returns an empty
443  *      array if there are none.
444  */
445 function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
446     global $DB;
447     $status_condition = array(
448         'all' => '',
449         'finished' => ' AND timefinish > 0',
450         'unfinished' => ' AND timefinish = 0'
451     );
452     $previewclause = '';
453     if (!$includepreviews) {
454         $previewclause = ' AND preview = 0';
455     }
456     return $DB->get_records_select('quiz_attempts',
457             'quiz = ? AND userid = ?' . $previewclause . $status_condition[$status],
458             array($quizid, $userid), 'attempt ASC');
461 /**
462  * Return grade for given user or all users.
463  *
464  * @param int $quizid id of quiz
465  * @param int $userid optional user id, 0 means all users
466  * @return array array of grades, false if none. These are raw grades. They should
467  * be processed with quiz_format_grade for display.
468  */
469 function quiz_get_user_grades($quiz, $userid = 0) {
470     global $CFG, $DB;
472     $params = array($quiz->id);
473     $usertest = '';
474     if ($userid) {
475         $params[] = $userid;
476         $usertest = 'AND u.id = ?';
477     }
478     return $DB->get_records_sql("
479             SELECT
480                 u.id,
481                 u.id AS userid,
482                 qg.grade AS rawgrade,
483                 qg.timemodified AS dategraded,
484                 MAX(qa.timefinish) AS datesubmitted
486             FROM {user} u
487             JOIN {quiz_grades} qg ON u.id = qg.userid
488             JOIN {quiz_attempts} qa ON qa.quiz = qg.quiz AND qa.userid = u.id
490             WHERE qg.quiz = ?
491             $usertest
492             GROUP BY u.id, qg.grade, qg.timemodified", $params);
495 /**
496  * Round a grade to to the correct number of decimal places, and format it for display.
497  *
498  * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
499  * @param float $grade The grade to round.
500  * @return float
501  */
502 function quiz_format_grade($quiz, $grade) {
503     if (is_null($grade)) {
504         return get_string('notyetgraded', 'quiz');
505     }
506     return format_float($grade, $quiz->decimalpoints);
509 /**
510  * Round a grade to to the correct number of decimal places, and format it for display.
511  *
512  * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
513  * @param float $grade The grade to round.
514  * @return float
515  */
516 function quiz_format_question_grade($quiz, $grade) {
517     if (empty($quiz->questiondecimalpoints)) {
518         $quiz->questiondecimalpoints = -1;
519     }
520     if ($quiz->questiondecimalpoints == -1) {
521         return format_float($grade, $quiz->decimalpoints);
522     } else {
523         return format_float($grade, $quiz->questiondecimalpoints);
524     }
527 /**
528  * Update grades in central gradebook
529  *
530  * @param object $quiz the quiz settings.
531  * @param int $userid specific user only, 0 means all users.
532  */
533 function quiz_update_grades($quiz, $userid = 0, $nullifnone = true) {
534     global $CFG, $DB;
535     require_once($CFG->libdir.'/gradelib.php');
537     if ($quiz->grade == 0) {
538         quiz_grade_item_update($quiz);
540     } else if ($grades = quiz_get_user_grades($quiz, $userid)) {
541         quiz_grade_item_update($quiz, $grades);
543     } else if ($userid && $nullifnone) {
544         $grade = new stdClass();
545         $grade->userid = $userid;
546         $grade->rawgrade = null;
547         quiz_grade_item_update($quiz, $grade);
549     } else {
550         quiz_grade_item_update($quiz);
551     }
554 /**
555  * Update all grades in gradebook.
556  */
557 function quiz_upgrade_grades() {
558     global $DB;
560     $sql = "SELECT COUNT('x')
561               FROM {quiz} a, {course_modules} cm, {modules} m
562              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
563     $count = $DB->count_records_sql($sql);
565     $sql = "SELECT a.*, cm.idnumber AS cmidnumber, a.course AS courseid
566               FROM {quiz} a, {course_modules} cm, {modules} m
567              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
568     $rs = $DB->get_recordset_sql($sql);
569     if ($rs->valid()) {
570         $pbar = new progress_bar('quizupgradegrades', 500, true);
571         $i=0;
572         foreach ($rs as $quiz) {
573             $i++;
574             upgrade_set_timeout(60*5); // set up timeout, may also abort execution
575             quiz_update_grades($quiz, 0, false);
576             $pbar->update($i, $count, "Updating Quiz grades ($i/$count).");
577         }
578     }
579     $rs->close();
582 /**
583  * Create grade item for given quiz
584  *
585  * @param object $quiz object with extra cmidnumber
586  * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
587  * @return int 0 if ok, error code otherwise
588  */
589 function quiz_grade_item_update($quiz, $grades = null) {
590     global $CFG, $OUTPUT;
591     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
592     require_once($CFG->libdir.'/gradelib.php');
594     if (array_key_exists('cmidnumber', $quiz)) { // may not be always present
595         $params = array('itemname' => $quiz->name, 'idnumber' => $quiz->cmidnumber);
596     } else {
597         $params = array('itemname' => $quiz->name);
598     }
600     if ($quiz->grade > 0) {
601         $params['gradetype'] = GRADE_TYPE_VALUE;
602         $params['grademax']  = $quiz->grade;
603         $params['grademin']  = 0;
605     } else {
606         $params['gradetype'] = GRADE_TYPE_NONE;
607     }
609     // description by TJ:
610     // 1. If the quiz is set to not show grades while the quiz is still open,
611     //    and is set to show grades after the quiz is closed, then create the
612     //    grade_item with a show-after date that is the quiz close date.
613     // 2. If the quiz is set to not show grades at either of those times,
614     //    create the grade_item as hidden.
615     // 3. If the quiz is set to show grades, create the grade_item visible.
616     $openreviewoptions = mod_quiz_display_options::make_from_quiz($quiz,
617             mod_quiz_display_options::LATER_WHILE_OPEN);
618     $closedreviewoptions = mod_quiz_display_options::make_from_quiz($quiz,
619             mod_quiz_display_options::AFTER_CLOSE);
620     if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX &&
621             $closedreviewoptions->marks < question_display_options::MARK_AND_MAX) {
622         $params['hidden'] = 1;
624     } else if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX &&
625             $closedreviewoptions->marks >= question_display_options::MARK_AND_MAX) {
626         if ($quiz->timeclose) {
627             $params['hidden'] = $quiz->timeclose;
628         } else {
629             $params['hidden'] = 1;
630         }
632     } else {
633         // a) both open and closed enabled
634         // b) open enabled, closed disabled - we can not "hide after",
635         //    grades are kept visible even after closing
636         $params['hidden'] = 0;
637     }
639     if ($grades  === 'reset') {
640         $params['reset'] = true;
641         $grades = null;
642     }
644     $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
645     if (!empty($gradebook_grades->items)) {
646         $grade_item = $gradebook_grades->items[0];
647         if ($grade_item->locked) {
648             $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
649             if (!$confirm_regrade) {
650                 $message = get_string('gradeitemislocked', 'grades');
651                 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id .
652                         '&amp;mode=overview';
653                 $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
654                 echo $OUTPUT->box_start('generalbox', 'notice');
655                 echo '<p>'. $message .'</p>';
656                 echo $OUTPUT->container_start('buttons');
657                 echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades'));
658                 echo $OUTPUT->single_button($back_link,  get_string('cancel'));
659                 echo $OUTPUT->container_end();
660                 echo $OUTPUT->box_end();
662                 return GRADE_UPDATE_ITEM_LOCKED;
663             }
664         }
665     }
667     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
670 /**
671  * Delete grade item for given quiz
672  *
673  * @param object $quiz object
674  * @return object quiz
675  */
676 function quiz_grade_item_delete($quiz) {
677     global $CFG;
678     require_once($CFG->libdir . '/gradelib.php');
680     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0,
681             null, array('deleted' => 1));
684 /**
685  * Returns an array of users who have data in a given quiz
686  *
687  * @todo: deprecated - to be deleted in 2.2
688  *
689  * @param int $quizid the quiz id.
690  * @return array of userids.
691  */
692 function quiz_get_participants($quizid) {
693     global $CFG, $DB;
695     return $DB->get_records_sql('
696             SELECT DISTINCT userid, userid
697             JOIN {quiz_attempts} qa
698             WHERE a.quiz = ?', array($quizid));
701 /**
702  * This standard function will check all instances of this module
703  * and make sure there are up-to-date events created for each of them.
704  * If courseid = 0, then every quiz event in the site is checked, else
705  * only quiz events belonging to the course specified are checked.
706  * This function is used, in its new format, by restore_refresh_events()
707  *
708  * @param int $courseid
709  * @return bool
710  */
711 function quiz_refresh_events($courseid = 0) {
712     global $DB;
714     if ($courseid == 0) {
715         if (!$quizzes = $DB->get_records('quiz')) {
716             return true;
717         }
718     } else {
719         if (!$quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
720             return true;
721         }
722     }
724     foreach ($quizzes as $quiz) {
725         quiz_update_events($quiz);
726     }
728     return true;
731 /**
732  * Returns all quiz graded users since a given time for specified quiz
733  */
734 function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
735         $courseid, $cmid, $userid = 0, $groupid = 0) {
736     global $CFG, $COURSE, $USER, $DB;
737     require_once('locallib.php');
739     if ($COURSE->id == $courseid) {
740         $course = $COURSE;
741     } else {
742         $course = $DB->get_record('course', array('id' => $courseid));
743     }
745     $modinfo =& get_fast_modinfo($course);
747     $cm = $modinfo->cms[$cmid];
748     $quiz = $DB->get_record('quiz', array('id' => $cm->instance));
750     if ($userid) {
751         $userselect = "AND u.id = :userid";
752         $params['userid'] = $userid;
753     } else {
754         $userselect = '';
755     }
757     if ($groupid) {
758         $groupselect = 'AND gm.groupid = :groupid';
759         $groupjoin   = 'JOIN {groups_members} gm ON  gm.userid=u.id';
760         $params['groupid'] = $groupid;
761     } else {
762         $groupselect = '';
763         $groupjoin   = '';
764     }
766     $params['timestart'] = $timestart;
767     $params['quizid'] = $quiz->id;
769     if (!$attempts = $DB->get_records_sql("
770               SELECT qa.*,
771                      u.firstname, u.lastname, u.email, u.picture, u.imagealt
772                 FROM {quiz_attempts} qa
773                      JOIN {user} u ON u.id = qa.userid
774                      $groupjoin
775                WHERE qa.timefinish > :timestart
776                  AND qa.quiz = :quizid
777                  AND qa.preview = 0
778                      $userselect
779                      $groupselect
780             ORDER BY qa.timefinish ASC", $params)) {
781         return;
782     }
784     $context         = get_context_instance(CONTEXT_MODULE, $cm->id);
785     $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
786     $viewfullnames   = has_capability('moodle/site:viewfullnames', $context);
787     $grader          = has_capability('mod/quiz:viewreports', $context);
788     $groupmode       = groups_get_activity_groupmode($cm, $course);
790     if (is_null($modinfo->groups)) {
791         // load all my groups and cache it in modinfo
792         $modinfo->groups = groups_get_user_groups($course->id);
793     }
795     $usersgroups = null;
796     $aname = format_string($cm->name, true);
797     foreach ($attempts as $attempt) {
798         if ($attempt->userid != $USER->id) {
799             if (!$grader) {
800                 // Grade permission required
801                 continue;
802             }
804             if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
805                 if (is_null($usersgroups)) {
806                     $usersgroups = groups_get_all_groups($course->id,
807                             $attempt->userid, $cm->groupingid);
808                     if (is_array($usersgroups)) {
809                         $usersgroups = array_keys($usersgroups);
810                     } else {
811                         $usersgroups = array();
812                     }
813                 }
814                 if (!array_intersect($usersgroups, $modinfo->groups[$cm->id])) {
815                     continue;
816                 }
817             }
818         }
820         $options = quiz_get_review_options($quiz, $attempt, $context);
822         $tmpactivity = new stdClass();
824         $tmpactivity->type       = 'quiz';
825         $tmpactivity->cmid       = $cm->id;
826         $tmpactivity->name       = $aname;
827         $tmpactivity->sectionnum = $cm->sectionnum;
828         $tmpactivity->timestamp  = $attempt->timefinish;
830         $tmpactivity->content->attemptid = $attempt->id;
831         $tmpactivity->content->attempt   = $attempt->attempt;
832         if (quiz_has_grades($quiz) && $options->marks >= question_display_options::MARK_AND_MAX) {
833             $tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades);
834             $tmpactivity->content->maxgrade  = quiz_format_grade($quiz, $quiz->sumgrades);
835         } else {
836             $tmpactivity->content->sumgrades = null;
837             $tmpactivity->content->maxgrade  = null;
838         }
840         $tmpactivity->user->id        = $attempt->userid;
841         $tmpactivity->user->firstname = $attempt->firstname;
842         $tmpactivity->user->lastname  = $attempt->lastname;
843         $tmpactivity->user->fullname  = fullname($attempt, $viewfullnames);
844         $tmpactivity->user->picture   = $attempt->picture;
845         $tmpactivity->user->imagealt  = $attempt->imagealt;
846         $tmpactivity->user->email     = $attempt->email;
848         $activities[$index++] = $tmpactivity;
849     }
852 function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
853     global $CFG, $OUTPUT;
855     echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">';
857     echo '<tr><td class="userpicture" valign="top">';
858     echo $OUTPUT->user_picture($activity->user, array('courseid' => $courseid));
859     echo '</td><td>';
861     if ($detail) {
862         $modname = $modnames[$activity->type];
863         echo '<div class="title">';
864         echo '<img src="' . $OUTPUT->pix_url('icon', $activity->type) . '" ' .
865                 'class="icon" alt="' . $modname . '" />';
866         echo '<a href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
867                 $activity->cmid . '">' . $activity->name . '</a>';
868         echo '</div>';
869     }
871     echo '<div class="grade">';
872     echo  get_string('attempt', 'quiz', $activity->content->attempt);
873     if (isset($activity->content->maxgrade)) {
874         $grades = $activity->content->sumgrades . ' / ' . $activity->content->maxgrade;
875         echo ': (<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?attempt=' .
876                 $activity->content->attemptid . '">' . $grades . '</a>)';
877     }
878     echo '</div>';
880     echo '<div class="user">';
881     echo '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $activity->user->id .
882             '&amp;course=' . $courseid . '">' . $activity->user->fullname .
883             '</a> - ' . userdate($activity->timestamp);
884     echo '</div>';
886     echo '</td></tr></table>';
888     return;
891 /**
892  * Pre-process the quiz options form data, making any necessary adjustments.
893  * Called by add/update instance in this file.
894  *
895  * @param object $quiz The variables set on the form.
896  */
897 function quiz_process_options($quiz) {
898     global $CFG;
899     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
900     require_once($CFG->libdir . '/questionlib.php');
902     $quiz->timemodified = time();
904     // Quiz name.
905     if (!empty($quiz->name)) {
906         $quiz->name = trim($quiz->name);
907     }
909     // Password field - different in form to stop browsers that remember passwords
910     // getting confused.
911     $quiz->password = $quiz->quizpassword;
912     unset($quiz->quizpassword);
914     // Quiz feedback
915     if (isset($quiz->feedbacktext)) {
916         // Clean up the boundary text.
917         for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
918             if (empty($quiz->feedbacktext[$i]['text'])) {
919                 $quiz->feedbacktext[$i]['text'] = '';
920             } else {
921                 $quiz->feedbacktext[$i]['text'] = trim($quiz->feedbacktext[$i]['text']);
922             }
923         }
925         // Check the boundary value is a number or a percentage, and in range.
926         $i = 0;
927         while (!empty($quiz->feedbackboundaries[$i])) {
928             $boundary = trim($quiz->feedbackboundaries[$i]);
929             if (!is_numeric($boundary)) {
930                 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
931                     $boundary = trim(substr($boundary, 0, -1));
932                     if (is_numeric($boundary)) {
933                         $boundary = $boundary * $quiz->grade / 100.0;
934                     } else {
935                         return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
936                     }
937                 }
938             }
939             if ($boundary <= 0 || $boundary >= $quiz->grade) {
940                 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
941             }
942             if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
943                 return get_string('feedbackerrororder', 'quiz', $i + 1);
944             }
945             $quiz->feedbackboundaries[$i] = $boundary;
946             $i += 1;
947         }
948         $numboundaries = $i;
950         // Check there is nothing in the remaining unused fields.
951         if (!empty($quiz->feedbackboundaries)) {
952             for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
953                 if (!empty($quiz->feedbackboundaries[$i]) &&
954                         trim($quiz->feedbackboundaries[$i]) != '') {
955                     return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
956                 }
957             }
958         }
959         for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
960             if (!empty($quiz->feedbacktext[$i]['text']) &&
961                     trim($quiz->feedbacktext[$i]['text']) != '') {
962                 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
963             }
964         }
965         // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
966         $quiz->feedbackboundaries[-1] = $quiz->grade + 1;
967         $quiz->feedbackboundaries[$numboundaries] = 0;
968         $quiz->feedbackboundarycount = $numboundaries;
969     }
971     // Combing the individual settings into the review columns.
972     $quiz->reviewattempt = quiz_review_option_form_to_db($quiz, 'attempt');
973     $quiz->reviewcorrectness = quiz_review_option_form_to_db($quiz, 'correctness');
974     $quiz->reviewmarks = quiz_review_option_form_to_db($quiz, 'marks');
975     $quiz->reviewspecificfeedback = quiz_review_option_form_to_db($quiz, 'specificfeedback');
976     $quiz->reviewgeneralfeedback = quiz_review_option_form_to_db($quiz, 'generalfeedback');
977     $quiz->reviewrightanswer = quiz_review_option_form_to_db($quiz, 'rightanswer');
978     $quiz->reviewoverallfeedback = quiz_review_option_form_to_db($quiz, 'overallfeedback');
979     $quiz->reviewattempt |= mod_quiz_display_options::DURING;
980     $quiz->reviewoverallfeedback &= ~mod_quiz_display_options::DURING;
983 /**
984  * Helper function for {@link quiz_process_options()}.
985  * @param object $fromform the sumbitted form date.
986  * @param string $field one of the review option field names.
987  */
988 function quiz_review_option_form_to_db($fromform, $field) {
989     static $times = array(
990         'during' => mod_quiz_display_options::DURING,
991         'immediately' => mod_quiz_display_options::IMMEDIATELY_AFTER,
992         'open' => mod_quiz_display_options::LATER_WHILE_OPEN,
993         'closed' => mod_quiz_display_options::AFTER_CLOSE,
994     );
996     $review = 0;
997     foreach ($times as $whenname => $when) {
998         $fieldname = $field . $whenname;
999         if (isset($fromform->$fieldname)) {
1000             $review |= $when;
1001             unset($fromform->$fieldname);
1002         }
1003     }
1005     return $review;
1008 /**
1009  * This function is called at the end of quiz_add_instance
1010  * and quiz_update_instance, to do the common processing.
1011  *
1012  * @param object $quiz the quiz object.
1013  */
1014 function quiz_after_add_or_update($quiz) {
1015     global $DB;
1016     $cmid = $quiz->coursemodule;
1018     // we need to use context now, so we need to make sure all needed info is already in db
1019     $DB->set_field('course_modules', 'instance', $quiz->id, array('id'=>$cmid));
1020     $context = get_context_instance(CONTEXT_MODULE, $cmid);
1022     // Save the feedback
1023     $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
1025     for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) {
1026         $feedback = new stdClass();
1027         $feedback->quizid = $quiz->id;
1028         $feedback->feedbacktext = $quiz->feedbacktext[$i]['text'];
1029         $feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format'];
1030         $feedback->mingrade = $quiz->feedbackboundaries[$i];
1031         $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
1032         $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1033         $feedbacktext = file_save_draft_area_files((int)$quiz->feedbacktext[$i]['itemid'],
1034                 $context->id, 'mod_quiz', 'feedback', $feedback->id,
1035                 array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0),
1036                 $quiz->feedbacktext[$i]['text']);
1037         $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext,
1038                 array('id' => $feedback->id));
1039     }
1041     // Update the events relating to this quiz.
1042     quiz_update_events($quiz);
1044     //update related grade item
1045     quiz_grade_item_update($quiz);
1048 /**
1049  * This function updates the events associated to the quiz.
1050  * If $override is non-zero, then it updates only the events
1051  * associated with the specified override.
1052  *
1053  * @uses QUIZ_MAX_EVENT_LENGTH
1054  * @param object $quiz the quiz object.
1055  * @param object optional $override limit to a specific override
1056  */
1057 function quiz_update_events($quiz, $override = null) {
1058     global $DB;
1060     // Load the old events relating to this quiz.
1061     $conds = array('modulename'=>'quiz',
1062                    'instance'=>$quiz->id);
1063     if (!empty($override)) {
1064         // only load events for this override
1065         $conds['groupid'] = isset($override->groupid)?  $override->groupid : 0;
1066         $conds['userid'] = isset($override->userid)?  $override->userid : 0;
1067     }
1068     $oldevents = $DB->get_records('event', $conds);
1070     // Now make a todo list of all that needs to be updated
1071     if (empty($override)) {
1072         // We are updating the primary settings for the quiz, so we
1073         // need to add all the overrides
1074         $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id));
1075         // as well as the original quiz (empty override)
1076         $overrides[] = new stdClass();
1077     } else {
1078         // Just do the one override
1079         $overrides = array($override);
1080     }
1082     foreach ($overrides as $current) {
1083         $groupid   = isset($current->groupid)?  $current->groupid : 0;
1084         $userid    = isset($current->userid)? $current->userid : 0;
1085         $timeopen  = isset($current->timeopen)?  $current->timeopen : $quiz->timeopen;
1086         $timeclose = isset($current->timeclose)? $current->timeclose : $quiz->timeclose;
1088         // only add open/close events for an override if they differ from the quiz default
1089         $addopen  = empty($current->id) || !empty($current->timeopen);
1090         $addclose = empty($current->id) || !empty($current->timeclose);
1092         $event = new stdClass();
1093         $event->description = $quiz->intro;
1094         // Events module won't show user events when the courseid is nonzero
1095         $event->courseid    = ($userid) ? 0 : $quiz->course;
1096         $event->groupid     = $groupid;
1097         $event->userid      = $userid;
1098         $event->modulename  = 'quiz';
1099         $event->instance    = $quiz->id;
1100         $event->timestart   = $timeopen;
1101         $event->timeduration = max($timeclose - $timeopen, 0);
1102         $event->visible     = instance_is_visible('quiz', $quiz);
1103         $event->eventtype   = 'open';
1105         // Determine the event name
1106         if ($groupid) {
1107             $params = new stdClass();
1108             $params->quiz = $quiz->name;
1109             $params->group = groups_get_group_name($groupid);
1110             if ($params->group === false) {
1111                 // group doesn't exist, just skip it
1112                 continue;
1113             }
1114             $eventname = get_string('overridegroupeventname', 'quiz', $params);
1115         } else if ($userid) {
1116             $params = new stdClass();
1117             $params->quiz = $quiz->name;
1118             $eventname = get_string('overrideusereventname', 'quiz', $params);
1119         } else {
1120             $eventname = $quiz->name;
1121         }
1122         if ($addopen or $addclose) {
1123             if ($timeclose and $timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) {
1124                 // Single event for the whole quiz.
1125                 if ($oldevent = array_shift($oldevents)) {
1126                     $event->id = $oldevent->id;
1127                 } else {
1128                     unset($event->id);
1129                 }
1130                 $event->name = $eventname;
1131                 // calendar_event::create will reuse a db record if the id field is set
1132                 calendar_event::create($event);
1133             } else {
1134                 // Separate start and end events.
1135                 $event->timeduration  = 0;
1136                 if ($timeopen && $addopen) {
1137                     if ($oldevent = array_shift($oldevents)) {
1138                         $event->id = $oldevent->id;
1139                     } else {
1140                         unset($event->id);
1141                     }
1142                     $event->name = $eventname.' ('.get_string('quizopens', 'quiz').')';
1143                     // calendar_event::create will reuse a db record if the id field is set
1144                     calendar_event::create($event);
1145                 }
1146                 if ($timeclose && $addclose) {
1147                     if ($oldevent = array_shift($oldevents)) {
1148                         $event->id = $oldevent->id;
1149                     } else {
1150                         unset($event->id);
1151                     }
1152                     $event->name      = $eventname.' ('.get_string('quizcloses', 'quiz').')';
1153                     $event->timestart = $timeclose;
1154                     $event->eventtype = 'close';
1155                     calendar_event::create($event);
1156                 }
1157             }
1158         }
1159     }
1161     // Delete any leftover events
1162     foreach ($oldevents as $badevent) {
1163         $badevent = calendar_event::load($badevent);
1164         $badevent->delete();
1165     }
1168 /**
1169  * @return array
1170  */
1171 function quiz_get_view_actions() {
1172     return array('view', 'view all', 'report', 'review');
1175 /**
1176  * @return array
1177  */
1178 function quiz_get_post_actions() {
1179     return array('attempt', 'close attempt', 'preview', 'editquestions',
1180             'delete attempt', 'manualgrade');
1183 /**
1184  * @param array $questionids of question ids.
1185  * @return bool whether any of these questions are used by any instance of this module.
1186  */
1187 function quiz_questions_in_use($questionids) {
1188     global $DB, $CFG;
1189     require_once($CFG->libdir . '/questionlib.php');
1190     list($test, $params) = $DB->get_in_or_equal($questionids);
1191     return $DB->record_exists_select('quiz_question_instances',
1192             'question ' . $test, $params) || question_engine::questions_in_use(
1193             $questionids, new qubaid_join('{quiz_attempts} quiza',
1194             'quiza.uniqueid', 'quiza.preview = 0'));
1197 /**
1198  * Implementation of the function for printing the form elements that control
1199  * whether the course reset functionality affects the quiz.
1200  *
1201  * @param $mform the course reset form that is being built.
1202  */
1203 function quiz_reset_course_form_definition($mform) {
1204     $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz'));
1205     $mform->addElement('advcheckbox', 'reset_quiz_attempts',
1206             get_string('removeallquizattempts', 'quiz'));
1209 /**
1210  * Course reset form defaults.
1211  * @return array the defaults.
1212  */
1213 function quiz_reset_course_form_defaults($course) {
1214     return array('reset_quiz_attempts' => 1);
1217 /**
1218  * Removes all grades from gradebook
1219  *
1220  * @param int $courseid
1221  * @param string optional type
1222  */
1223 function quiz_reset_gradebook($courseid, $type='') {
1224     global $CFG, $DB;
1226     $quizzes = $DB->get_records_sql("
1227             SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
1228             FROM {modules} m
1229             JOIN {course_modules} cm ON m.id = cm.module
1230             JOIN {quiz} q ON cm.instance = q.id
1231             WHERE m.name = 'quiz' AND cm.course = ?", array($courseid));
1233     foreach ($quizzes as $quiz) {
1234         quiz_grade_item_update($quiz, 'reset');
1235     }
1238 /**
1239  * Actual implementation of the reset course functionality, delete all the
1240  * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is
1241  * set and true.
1242  *
1243  * Also, move the quiz open and close dates, if the course start date is changing.
1244  *
1245  * @param object $data the data submitted from the reset course.
1246  * @return array status array
1247  */
1248 function quiz_reset_userdata($data) {
1249     global $CFG, $DB;
1250     require_once($CFG->libdir.'/questionlib.php');
1252     $componentstr = get_string('modulenameplural', 'quiz');
1253     $status = array();
1255     // Delete attempts.
1256     if (!empty($data->reset_quiz_attempts)) {
1257         require_once($CFG->libdir . '/questionlib.php');
1259         question_engine::delete_questions_usage_by_activities(new qubaid_join(
1260                 '{quiz_attempts} quiza JOIN {quiz} quiz ON quiza.quiz = quiz.id',
1261                 'quiza.uniqueid', 'quiz.course = :quizcourseid',
1262                 array('quizcourseid' => $data->courseid)));
1264         $DB->delete_records_select('quiz_attempts',
1265                 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid));
1266         $status[] = array(
1267             'component' => $componentstr,
1268             'item' => get_string('attemptsdeleted', 'quiz'),
1269             'error' => false);
1271         // Remove all grades from gradebook
1272         if (empty($data->reset_gradebook_grades)) {
1273             quiz_reset_gradebook($data->courseid);
1274         }
1275         $status[] = array(
1276             'component' => $componentstr,
1277             'item' => get_string('attemptsdeleted', 'quiz'),
1278             'error' => false);
1279     }
1281     // Updating dates - shift may be negative too
1282     if ($data->timeshift) {
1283         shift_course_mod_dates('quiz', array('timeopen', 'timeclose'),
1284                 $data->timeshift, $data->courseid);
1285         $status[] = array(
1286             'component' => $componentstr,
1287             'item' => get_string('openclosedatesupdated', 'quiz'),
1288             'error' => false);
1289     }
1291     return $status;
1294 /**
1295  * Checks whether the current user is allowed to view a file uploaded in a quiz.
1296  * Teachers can view any from their courses, students can only view their own.
1297  *
1298  * @param int $attemptuniqueid int attempt id
1299  * @param int $questionid int question id
1300  * @return bool to indicate access granted or denied
1301  */
1302 function quiz_check_file_access($attemptuniqueid, $questionid, $context = null) {
1303     global $USER, $DB, $CFG;
1304     require_once(dirname(__FILE__).'/attemptlib.php');
1305     require_once(dirname(__FILE__).'/locallib.php');
1307     $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $attemptuniqueid));
1308     $attemptobj = quiz_attempt::create($attempt->id);
1310     // does question exist?
1311     if (!$question = $DB->get_record('question', array('id' => $questionid))) {
1312         return false;
1313     }
1315     if ($context === null) {
1316         $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz));
1317         $cm = get_coursemodule_from_id('quiz', $quiz->id);
1318         $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1319     }
1321     // Load those questions and the associated states.
1322     $attemptobj->load_questions(array($questionid));
1323     $attemptobj->load_question_states(array($questionid));
1325     // obtain state
1326     $state = $attemptobj->get_question_state($questionid);
1327     // obtain questoin
1328     $question = $attemptobj->get_question($questionid);
1330     // access granted if the current user submitted this file
1331     if ($attempt->userid != $USER->id) {
1332         return false;
1333     }
1334     // access granted if the current user has permission to grade quizzes in this course
1335     if (!(has_capability('mod/quiz:viewreports', $context) ||
1336             has_capability('mod/quiz:grade', $context))) {
1337         return false;
1338     }
1340     return array($question, $state, array());
1343 /**
1344  * Prints quiz summaries on MyMoodle Page
1345  * @param arry $courses
1346  * @param array $htmlarray
1347  */
1348 function quiz_print_overview($courses, &$htmlarray) {
1349     global $USER, $CFG;
1350     // These next 6 Lines are constant in all modules (just change module name)
1351     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1352         return array();
1353     }
1355     if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
1356         return;
1357     }
1359     // Fetch some language strings outside the main loop.
1360     $strquiz = get_string('modulename', 'quiz');
1361     $strnoattempts = get_string('noattempts', 'quiz');
1363     // We want to list quizzes that are currently available, and which have a close date.
1364     // This is the same as what the lesson does, and the dabate is in MDL-10568.
1365     $now = time();
1366     foreach ($quizzes as $quiz) {
1367         if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
1368             // Give a link to the quiz, and the deadline.
1369             $str = '<div class="quiz overview">' .
1370                     '<div class="name">' . $strquiz . ': <a ' .
1371                     ($quiz->visible ? '' : ' class="dimmed"') .
1372                     ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
1373                     $quiz->coursemodule . '">' .
1374                     $quiz->name . '</a></div>';
1375             $str .= '<div class="info">' . get_string('quizcloseson', 'quiz',
1376                     userdate($quiz->timeclose)) . '</div>';
1378             // Now provide more information depending on the uers's role.
1379             $context = get_context_instance(CONTEXT_MODULE, $quiz->coursemodule);
1380             if (has_capability('mod/quiz:viewreports', $context)) {
1381                 // For teacher-like people, show a summary of the number of student attempts.
1382                 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
1383                 // fields set to make the following call work.
1384                 $str .= '<div class="info">' .
1385                         quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
1386             } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
1387                     $context)) { // Student
1388                 // For student-like people, tell them how many attempts they have made.
1389                 if (isset($USER->id) &&
1390                         ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
1391                     $numattempts = count($attempts);
1392                     $str .= '<div class="info">' .
1393                             get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
1394                 } else {
1395                     $str .= '<div class="info">' . $strnoattempts . '</div>';
1396                 }
1397             } else {
1398                 // For ayone else, there is no point listing this quiz, so stop processing.
1399                 continue;
1400             }
1402             // Add the output for this quiz to the rest.
1403             $str .= '</div>';
1404             if (empty($htmlarray[$quiz->course]['quiz'])) {
1405                 $htmlarray[$quiz->course]['quiz'] = $str;
1406             } else {
1407                 $htmlarray[$quiz->course]['quiz'] .= $str;
1408             }
1409         }
1410     }
1413 /**
1414  * Return a textual summary of the number of attempts that have been made at a particular quiz,
1415  * returns '' if no attempts have been made yet, unless $returnzero is passed as true.
1416  *
1417  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1418  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and
1419  *      $cm->groupingid fields are used at the moment.
1420  * @param bool $returnzero if false (default), when no attempts have been
1421  *      made '' is returned instead of 'Attempts: 0'.
1422  * @param int $currentgroup if there is a concept of current group where this method is being called
1423  *         (e.g. a report) pass it in here. Default 0 which means no current group.
1424  * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
1425  *          "Attemtps 123 (45 from this group)".
1426  */
1427 function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
1428     global $DB, $USER;
1429     $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0));
1430     if ($numattempts || $returnzero) {
1431         if (groups_get_activity_groupmode($cm)) {
1432             $a->total = $numattempts;
1433             if ($currentgroup) {
1434                 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1435                         '{quiz_attempts} qa JOIN ' .
1436                         '{groups_members} gm ON qa.userid = gm.userid ' .
1437                         'WHERE quiz = ? AND preview = 0 AND groupid = ?',
1438                         array($quiz->id, $currentgroup));
1439                 return get_string('attemptsnumthisgroup', 'quiz', $a);
1440             } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
1441                 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups));
1442                 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1443                         '{quiz_attempts} qa JOIN ' .
1444                         '{groups_members} gm ON qa.userid = gm.userid ' .
1445                         'WHERE quiz = ? AND preview = 0 AND ' .
1446                         "groupid $usql", array_merge(array($quiz->id), $params));
1447                 return get_string('attemptsnumyourgroups', 'quiz', $a);
1448             }
1449         }
1450         return get_string('attemptsnum', 'quiz', $numattempts);
1451     }
1452     return '';
1455 /**
1456  * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
1457  * to the quiz reports.
1458  *
1459  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1460  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and
1461  *      $cm->groupingid fields are used at the moment.
1462  * @param object $context the quiz context.
1463  * @param bool $returnzero if false (default), when no attempts have been made
1464  *      '' is returned instead of 'Attempts: 0'.
1465  * @param int $currentgroup if there is a concept of current group where this method is being called
1466  *         (e.g. a report) pass it in here. Default 0 which means no current group.
1467  * @return string HTML fragment for the link.
1468  */
1469 function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false,
1470         $currentgroup = 0) {
1471     global $CFG;
1472     $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
1473     if (!$summary) {
1474         return '';
1475     }
1477     require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1478     $url = new moodle_url('/mod/quiz/report.php', array(
1479             'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
1480     return html_writer::link($url, $summary);
1483 /**
1484  * @param string $feature FEATURE_xx constant for requested feature
1485  * @return bool True if quiz supports feature
1486  */
1487 function quiz_supports($feature) {
1488     switch($feature) {
1489         case FEATURE_GROUPS:                  return true;
1490         case FEATURE_GROUPINGS:               return true;
1491         case FEATURE_GROUPMEMBERSONLY:        return true;
1492         case FEATURE_MOD_INTRO:               return true;
1493         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
1494         case FEATURE_GRADE_HAS_GRADE:         return true;
1495         case FEATURE_GRADE_OUTCOMES:          return true;
1496         case FEATURE_BACKUP_MOODLE2:          return true;
1498         default: return null;
1499     }
1502 /**
1503  * @return array all other caps used in module
1504  */
1505 function quiz_get_extra_capabilities() {
1506     global $CFG;
1507     require_once($CFG->libdir.'/questionlib.php');
1508     $caps = question_get_all_capabilities();
1509     $caps[] = 'moodle/site:accessallgroups';
1510     return $caps;
1513 /**
1514  * This fucntion extends the global navigation for the site.
1515  * It is important to note that you should not rely on PAGE objects within this
1516  * body of code as there is no guarantee that during an AJAX request they are
1517  * available
1518  *
1519  * @param navigation_node $quiznode The quiz node within the global navigation
1520  * @param object $course The course object returned from the DB
1521  * @param object $module The module object returned from the DB
1522  * @param object $cm The course module instance returned from the DB
1523  */
1524 function quiz_extend_navigation($quiznode, $course, $module, $cm) {
1525     global $CFG;
1527     $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1529     if (has_capability('mod/quiz:view', $context)) {
1530         $url = new moodle_url('/mod/quiz/view.php', array('id'=>$cm->id));
1531         $quiznode->add(get_string('info', 'quiz'), $url, navigation_node::TYPE_SETTING,
1532                 null, null, new pix_icon('i/info', ''));
1533     }
1535     if (has_any_capability(array('mod/quiz:viewreports', 'mod/quiz:grade'), $context)) {
1536         require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php');
1537         $reportlist = quiz_report_list($context);
1539         $url = new moodle_url('/mod/quiz/report.php',
1540                 array('id' => $cm->id, 'mode' => reset($reportlist)));
1541         $reportnode = $quiznode->add(get_string('results', 'quiz'), $url,
1542                 navigation_node::TYPE_SETTING,
1543                 null, null, new pix_icon('i/report', ''));
1545         foreach ($reportlist as $report) {
1546             $url = new moodle_url('/mod/quiz/report.php',
1547                     array('id' => $cm->id, 'mode' => $report));
1548             $reportnode->add(get_string($report, 'quiz_'.$report), $url,
1549                     navigation_node::TYPE_SETTING,
1550                     null, 'quiz_report_' . $report, new pix_icon('i/item', ''));
1551         }
1552     }
1555 /**
1556  * This function extends the settings navigation block for the site.
1557  *
1558  * It is safe to rely on PAGE here as we will only ever be within the module
1559  * context when this is called
1560  *
1561  * @param settings_navigation $settings
1562  * @param navigation_node $quiznode
1563  */
1564 function quiz_extend_settings_navigation($settings, $quiznode) {
1565     global $PAGE, $CFG;
1567     /**
1568      * Require {@link questionlib.php}
1569      * Included here as we only ever want to include this file if we really need to.
1570      */
1571     require_once($CFG->libdir . '/questionlib.php');
1573     // We want to add these new nodes after the Edit settings node, and before the
1574     // Locally assigned roles node. Of course, both of those are controlled by capabilities.
1575     $keys = $quiznode->get_children_key_list();
1576     $beforekey = null;
1577     $i = array_search('modedit', $keys);
1578     if ($i === false and array_key_exists(0, $keys)) {
1579         $beforekey = $keys[0];
1580     } else if (array_key_exists($i + 1, $keys)) {
1581         $beforekey = $keys[$i + 1];
1582     }
1584     if (has_capability('mod/quiz:manageoverrides', $PAGE->cm->context)) {
1585         $url = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$PAGE->cm->id));
1586         $node = navigation_node::create(get_string('groupoverrides', 'quiz'),
1587                 new moodle_url($url, array('mode'=>'group')),
1588                 navigation_node::TYPE_SETTING, null, 'mod_quiz_groupoverrides');
1589         $quiznode->add_node($node, $beforekey);
1591         $node = navigation_node::create(get_string('useroverrides', 'quiz'),
1592                 new moodle_url($url, array('mode'=>'user')),
1593                 navigation_node::TYPE_SETTING, null, 'mod_quiz_useroverrides');
1594         $quiznode->add_node($node, $beforekey);
1595     }
1597     if (has_capability('mod/quiz:manage', $PAGE->cm->context)) {
1598         $node = navigation_node::create(get_string('editquiz', 'quiz'),
1599                 new moodle_url('/mod/quiz/edit.php', array('cmid'=>$PAGE->cm->id)),
1600                 navigation_node::TYPE_SETTING, null, 'mod_quiz_edit',
1601                 new pix_icon('t/edit', ''));
1602         $quiznode->add_node($node, $beforekey);
1603     }
1605     if (has_capability('mod/quiz:preview', $PAGE->cm->context)) {
1606         $url = new moodle_url('/mod/quiz/startattempt.php',
1607                 array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey()));
1608         $node = navigation_node::create(get_string('preview', 'quiz'), $url,
1609                 navigation_node::TYPE_SETTING, null, 'mod_quiz_preview',
1610                 new pix_icon('t/preview', ''));
1611         $quiznode->add_node($node, $beforekey);
1612     }
1614     question_extend_settings_navigation($quiznode, $PAGE->cm->context)->trim_if_empty();
1617 /**
1618  * Serves the quiz files.
1619  *
1620  * @param object $course
1621  * @param object $cm
1622  * @param object $context
1623  * @param string $filearea
1624  * @param array $args
1625  * @param bool $forcedownload
1626  * @return bool false if file not found, does not return if found - justsend the file
1627  */
1628 function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload) {
1629     global $CFG, $DB;
1631     if ($context->contextlevel != CONTEXT_MODULE) {
1632         return false;
1633     }
1635     require_login($course, false, $cm);
1637     if (!$quiz = $DB->get_record('quiz', array('id'=>$cm->instance))) {
1638         return false;
1639     }
1641     // 'intro' area is served by pluginfile.php
1642     $fileareas = array('feedback');
1643     if (!in_array($filearea, $fileareas)) {
1644         return false;
1645     }
1647     $feedbackid = (int)array_shift($args);
1648     if (!$feedback = $DB->get_record('quiz_feedback', array('id'=>$feedbackid))) {
1649         return false;
1650     }
1652     $fs = get_file_storage();
1653     $relativepath = implode('/', $args);
1654     $fullpath = "/$context->id/mod_quiz/$filearea/$feedbackid/$relativepath";
1655     if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1656         return false;
1657     }
1658     send_stored_file($file, 0, 0, true);
1661 /**
1662  * Called via pluginfile.php -> question_pluginfile to serve files belonging to
1663  * a question in a question_attempt when that attempt is a quiz attempt.
1664  *
1665  * @param object $course course settings object
1666  * @param object $context context object
1667  * @param string $component the name of the component we are serving files for.
1668  * @param string $filearea the name of the file area.
1669  * @param array $args the remaining bits of the file path.
1670  * @param bool $forcedownload whether the user must be forced to download the file.
1671  * @return bool false if file not found, does not return if found - justsend the file
1672  */
1673 function mod_quiz_question_pluginfile($course, $context, $component,
1674         $filearea, $qubaid, $slot, $args, $forcedownload) {
1675     global $USER, $CFG;
1676     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
1678     $attemptobj = quiz_attempt::create_from_usage_id($qubaid);
1679     require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
1681     if ($attemptobj->is_own_attempt() && !$attemptobj->is_finished()) {
1682         // In the middle of an attempt.
1683         if (!$attemptobj->is_preview_user()) {
1684             $attemptobj->require_capability('mod/quiz:attempt');
1685         }
1686         $isreviewing = false;
1688     } else {
1689         // Reviewing an attempt.
1690         $attemptobj->check_review_capability();
1691         $isreviewing = true;
1692     }
1694     if (!$attemptobj->check_file_access($slot, $isreviewing, $context->id,
1695             $component, $filearea, $args, $forcedownload)) {
1696         send_file_not_found();
1697     }
1699     $fs = get_file_storage();
1700     $relativepath = implode('/', $args);
1701     $fullpath = "/$context->id/$component/$filearea/$relativepath";
1702     if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1703         send_file_not_found();
1704     }
1706     send_stored_file($file, 0, 0, $forcedownload);
1709 /**
1710  * Return a list of page types
1711  * @param string $pagetype current page type
1712  * @param stdClass $parentcontext Block's parent context
1713  * @param stdClass $currentcontext Current context of block
1714  */
1715 function quiz_page_type_list($pagetype, $parentcontext, $currentcontext) {
1716     $module_pagetype = array(
1717         'mod-quiz-*'=>get_string('page-mod-quiz-x', 'quiz'),
1718         'mod-quiz-edit'=>get_string('page-mod-quiz-edit', 'quiz'));
1719     return $module_pagetype;