Merge branch 'MDL-33400' of git://github.com/nebgor/moodle
[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;
449     mtrace('');
451     // Since the quiz specifies $module->cron = 60, so that the subplugins can
452     // have frequent cron if they need it, we now need to do our own scheduling.
453     $quizconfig = get_config('quiz');
454     if (!isset($quizconfig->overduelastrun)) {
455         $quizconfig->overduelastrun = 0;
456         $quizconfig->overduedoneto  = 0;
457     }
459     $timenow = time();
460     if ($timenow > $quizconfig->overduelastrun + 3600) {
461         require_once($CFG->dirroot . '/mod/quiz/cronlib.php');
462         $overduehander = new mod_quiz_overdue_attempt_updater();
464         $processto = $timenow - $quizconfig->graceperiodmin;
466         mtrace('  Looking for quiz overdue quiz attempts between ' .
467                 userdate($quizconfig->overduedoneto) . ' and ' . userdate($processto) . '...');
469         list($count, $quizcount) = $overduehander->update_overdue_attempts($timenow, $quizconfig->overduedoneto, $processto);
470         set_config('overduelastrun', $timenow, 'quiz');
471         set_config('overduedoneto', $processto, 'quiz');
473         mtrace('  Considered ' . $count . ' attempts in ' . $quizcount . ' quizzes.');
474     }
476     // Run cron for our sub-plugin types.
477     cron_execute_plugin_type('quiz', 'quiz reports');
478     cron_execute_plugin_type('quizaccess', 'quiz access rules');
480     return true;
483 /**
484  * @param int $quizid the quiz id.
485  * @param int $userid the userid.
486  * @param string $status 'all', 'finished' or 'unfinished' to control
487  * @param bool $includepreviews
488  * @return an array of all the user's attempts at this quiz. Returns an empty
489  *      array if there are none.
490  */
491 function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
492     global $DB, $CFG;
493     // TODO MDL-33071 it is very annoying to have to included all of locallib.php
494     // just to get the quiz_attempt::FINISHED constants, but I will try to sort
495     // that out properly for Moodle 2.4. For now, I will just do a quick fix for
496     // MDL-33048.
497     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
499     $params = array();
500     switch ($status) {
501         case 'all':
502             $statuscondition = '';
503             break;
505         case 'finished':
506             $statuscondition = ' AND state IN (:state1, :state2)';
507             $params['state1'] = quiz_attempt::FINISHED;
508             $params['state2'] = quiz_attempt::ABANDONED;
509             break;
511         case 'unfinished':
512             $statuscondition = ' AND state IN (:state1, :state2)';
513             $params['state1'] = quiz_attempt::IN_PROGRESS;
514             $params['state2'] = quiz_attempt::OVERDUE;
515             break;
516     }
518     $previewclause = '';
519     if (!$includepreviews) {
520         $previewclause = ' AND preview = 0';
521     }
523     $params['quizid'] = $quizid;
524     $params['userid'] = $userid;
525     return $DB->get_records_select('quiz_attempts',
526             'quiz = :quizid AND userid = :userid' . $previewclause . $statuscondition,
527             $params, 'attempt ASC');
530 /**
531  * Return grade for given user or all users.
532  *
533  * @param int $quizid id of quiz
534  * @param int $userid optional user id, 0 means all users
535  * @return array array of grades, false if none. These are raw grades. They should
536  * be processed with quiz_format_grade for display.
537  */
538 function quiz_get_user_grades($quiz, $userid = 0) {
539     global $CFG, $DB;
541     $params = array($quiz->id);
542     $usertest = '';
543     if ($userid) {
544         $params[] = $userid;
545         $usertest = 'AND u.id = ?';
546     }
547     return $DB->get_records_sql("
548             SELECT
549                 u.id,
550                 u.id AS userid,
551                 qg.grade AS rawgrade,
552                 qg.timemodified AS dategraded,
553                 MAX(qa.timefinish) AS datesubmitted
555             FROM {user} u
556             JOIN {quiz_grades} qg ON u.id = qg.userid
557             JOIN {quiz_attempts} qa ON qa.quiz = qg.quiz AND qa.userid = u.id
559             WHERE qg.quiz = ?
560             $usertest
561             GROUP BY u.id, qg.grade, qg.timemodified", $params);
564 /**
565  * Round a grade to to the correct number of decimal places, and format it for display.
566  *
567  * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
568  * @param float $grade The grade to round.
569  * @return float
570  */
571 function quiz_format_grade($quiz, $grade) {
572     if (is_null($grade)) {
573         return get_string('notyetgraded', 'quiz');
574     }
575     return format_float($grade, $quiz->decimalpoints);
578 /**
579  * Round a grade to to the correct number of decimal places, and format it for display.
580  *
581  * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
582  * @param float $grade The grade to round.
583  * @return float
584  */
585 function quiz_format_question_grade($quiz, $grade) {
586     if (empty($quiz->questiondecimalpoints)) {
587         $quiz->questiondecimalpoints = -1;
588     }
589     if ($quiz->questiondecimalpoints == -1) {
590         return format_float($grade, $quiz->decimalpoints);
591     } else {
592         return format_float($grade, $quiz->questiondecimalpoints);
593     }
596 /**
597  * Update grades in central gradebook
598  *
599  * @category grade
600  * @param object $quiz the quiz settings.
601  * @param int $userid specific user only, 0 means all users.
602  * @param bool $nullifnone If a single user is specified and $nullifnone is true a grade item with a null rawgrade will be inserted
603  */
604 function quiz_update_grades($quiz, $userid = 0, $nullifnone = true) {
605     global $CFG, $DB;
606     require_once($CFG->libdir.'/gradelib.php');
608     if ($quiz->grade == 0) {
609         quiz_grade_item_update($quiz);
611     } else if ($grades = quiz_get_user_grades($quiz, $userid)) {
612         quiz_grade_item_update($quiz, $grades);
614     } else if ($userid && $nullifnone) {
615         $grade = new stdClass();
616         $grade->userid = $userid;
617         $grade->rawgrade = null;
618         quiz_grade_item_update($quiz, $grade);
620     } else {
621         quiz_grade_item_update($quiz);
622     }
625 /**
626  * Update all grades in gradebook.
627  */
628 function quiz_upgrade_grades() {
629     global $DB;
631     $sql = "SELECT COUNT('x')
632               FROM {quiz} a, {course_modules} cm, {modules} m
633              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
634     $count = $DB->count_records_sql($sql);
636     $sql = "SELECT a.*, cm.idnumber AS cmidnumber, a.course AS courseid
637               FROM {quiz} a, {course_modules} cm, {modules} m
638              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
639     $rs = $DB->get_recordset_sql($sql);
640     if ($rs->valid()) {
641         $pbar = new progress_bar('quizupgradegrades', 500, true);
642         $i=0;
643         foreach ($rs as $quiz) {
644             $i++;
645             upgrade_set_timeout(60*5); // Set up timeout, may also abort execution.
646             quiz_update_grades($quiz, 0, false);
647             $pbar->update($i, $count, "Updating Quiz grades ($i/$count).");
648         }
649     }
650     $rs->close();
653 /**
654  * Create or update the grade item for given quiz
655  *
656  * @category grade
657  * @param object $quiz object with extra cmidnumber
658  * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
659  * @return int 0 if ok, error code otherwise
660  */
661 function quiz_grade_item_update($quiz, $grades = null) {
662     global $CFG, $OUTPUT;
663     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
664     require_once($CFG->libdir.'/gradelib.php');
666     if (array_key_exists('cmidnumber', $quiz)) { // May not be always present.
667         $params = array('itemname' => $quiz->name, 'idnumber' => $quiz->cmidnumber);
668     } else {
669         $params = array('itemname' => $quiz->name);
670     }
672     if ($quiz->grade > 0) {
673         $params['gradetype'] = GRADE_TYPE_VALUE;
674         $params['grademax']  = $quiz->grade;
675         $params['grademin']  = 0;
677     } else {
678         $params['gradetype'] = GRADE_TYPE_NONE;
679     }
681     // What this is trying to do:
682     // 1. If the quiz is set to not show grades while the quiz is still open,
683     //    and is set to show grades after the quiz is closed, then create the
684     //    grade_item with a show-after date that is the quiz close date.
685     // 2. If the quiz is set to not show grades at either of those times,
686     //    create the grade_item as hidden.
687     // 3. If the quiz is set to show grades, create the grade_item visible.
688     $openreviewoptions = mod_quiz_display_options::make_from_quiz($quiz,
689             mod_quiz_display_options::LATER_WHILE_OPEN);
690     $closedreviewoptions = mod_quiz_display_options::make_from_quiz($quiz,
691             mod_quiz_display_options::AFTER_CLOSE);
692     if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX &&
693             $closedreviewoptions->marks < question_display_options::MARK_AND_MAX) {
694         $params['hidden'] = 1;
696     } else if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX &&
697             $closedreviewoptions->marks >= question_display_options::MARK_AND_MAX) {
698         if ($quiz->timeclose) {
699             $params['hidden'] = $quiz->timeclose;
700         } else {
701             $params['hidden'] = 1;
702         }
704     } else {
705         // Either
706         // a) both open and closed enabled
707         // b) open enabled, closed disabled - we can not "hide after",
708         //    grades are kept visible even after closing.
709         $params['hidden'] = 0;
710     }
712     if ($grades  === 'reset') {
713         $params['reset'] = true;
714         $grades = null;
715     }
717     $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
718     if (!empty($gradebook_grades->items)) {
719         $grade_item = $gradebook_grades->items[0];
720         if ($grade_item->locked) {
721             // NOTE: this is an extremely nasty hack! It is not a bug if this confirmation fails badly. --skodak.
722             $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
723             if (!$confirm_regrade) {
724                 $message = get_string('gradeitemislocked', 'grades');
725                 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id .
726                         '&amp;mode=overview';
727                 $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
728                 echo $OUTPUT->box_start('generalbox', 'notice');
729                 echo '<p>'. $message .'</p>';
730                 echo $OUTPUT->container_start('buttons');
731                 echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades'));
732                 echo $OUTPUT->single_button($back_link,  get_string('cancel'));
733                 echo $OUTPUT->container_end();
734                 echo $OUTPUT->box_end();
736                 return GRADE_UPDATE_ITEM_LOCKED;
737             }
738         }
739     }
741     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
744 /**
745  * Delete grade item for given quiz
746  *
747  * @category grade
748  * @param object $quiz object
749  * @return object quiz
750  */
751 function quiz_grade_item_delete($quiz) {
752     global $CFG;
753     require_once($CFG->libdir . '/gradelib.php');
755     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0,
756             null, array('deleted' => 1));
759 /**
760  * This standard function will check all instances of this module
761  * and make sure there are up-to-date events created for each of them.
762  * If courseid = 0, then every quiz event in the site is checked, else
763  * only quiz events belonging to the course specified are checked.
764  * This function is used, in its new format, by restore_refresh_events()
765  *
766  * @param int $courseid
767  * @return bool
768  */
769 function quiz_refresh_events($courseid = 0) {
770     global $DB;
772     if ($courseid == 0) {
773         if (!$quizzes = $DB->get_records('quiz')) {
774             return true;
775         }
776     } else {
777         if (!$quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
778             return true;
779         }
780     }
782     foreach ($quizzes as $quiz) {
783         quiz_update_events($quiz);
784     }
786     return true;
789 /**
790  * Returns all quiz graded users since a given time for specified quiz
791  */
792 function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
793         $courseid, $cmid, $userid = 0, $groupid = 0) {
794     global $CFG, $COURSE, $USER, $DB;
795     require_once('locallib.php');
797     if ($COURSE->id == $courseid) {
798         $course = $COURSE;
799     } else {
800         $course = $DB->get_record('course', array('id' => $courseid));
801     }
803     $modinfo = get_fast_modinfo($course);
805     $cm = $modinfo->cms[$cmid];
806     $quiz = $DB->get_record('quiz', array('id' => $cm->instance));
808     if ($userid) {
809         $userselect = "AND u.id = :userid";
810         $params['userid'] = $userid;
811     } else {
812         $userselect = '';
813     }
815     if ($groupid) {
816         $groupselect = 'AND gm.groupid = :groupid';
817         $groupjoin   = 'JOIN {groups_members} gm ON  gm.userid=u.id';
818         $params['groupid'] = $groupid;
819     } else {
820         $groupselect = '';
821         $groupjoin   = '';
822     }
824     $params['timestart'] = $timestart;
825     $params['quizid'] = $quiz->id;
827     if (!$attempts = $DB->get_records_sql("
828               SELECT qa.*,
829                      u.firstname, u.lastname, u.email, u.picture, u.imagealt
830                 FROM {quiz_attempts} qa
831                      JOIN {user} u ON u.id = qa.userid
832                      $groupjoin
833                WHERE qa.timefinish > :timestart
834                  AND qa.quiz = :quizid
835                  AND qa.preview = 0
836                      $userselect
837                      $groupselect
838             ORDER BY qa.timefinish ASC", $params)) {
839         return;
840     }
842     $context         = get_context_instance(CONTEXT_MODULE, $cm->id);
843     $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
844     $viewfullnames   = has_capability('moodle/site:viewfullnames', $context);
845     $grader          = has_capability('mod/quiz:viewreports', $context);
846     $groupmode       = groups_get_activity_groupmode($cm, $course);
848     if (is_null($modinfo->groups)) {
849         // Load all my groups and cache it in modinfo.
850         $modinfo->groups = groups_get_user_groups($course->id);
851     }
853     $usersgroups = null;
854     $aname = format_string($cm->name, true);
855     foreach ($attempts as $attempt) {
856         if ($attempt->userid != $USER->id) {
857             if (!$grader) {
858                 // Grade permission required.
859                 continue;
860             }
862             if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
863                 if (is_null($usersgroups)) {
864                     $usersgroups = groups_get_all_groups($course->id,
865                             $attempt->userid, $cm->groupingid);
866                     if (is_array($usersgroups)) {
867                         $usersgroups = array_keys($usersgroups);
868                     } else {
869                         $usersgroups = array();
870                     }
871                 }
872                 if (!array_intersect($usersgroups, $modinfo->groups[$cm->id])) {
873                     continue;
874                 }
875             }
876         }
878         $options = quiz_get_review_options($quiz, $attempt, $context);
880         $tmpactivity = new stdClass();
882         $tmpactivity->type       = 'quiz';
883         $tmpactivity->cmid       = $cm->id;
884         $tmpactivity->name       = $aname;
885         $tmpactivity->sectionnum = $cm->sectionnum;
886         $tmpactivity->timestamp  = $attempt->timefinish;
888         $tmpactivity->content->attemptid = $attempt->id;
889         $tmpactivity->content->attempt   = $attempt->attempt;
890         if (quiz_has_grades($quiz) && $options->marks >= question_display_options::MARK_AND_MAX) {
891             $tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades);
892             $tmpactivity->content->maxgrade  = quiz_format_grade($quiz, $quiz->sumgrades);
893         } else {
894             $tmpactivity->content->sumgrades = null;
895             $tmpactivity->content->maxgrade  = null;
896         }
898         $tmpactivity->user->id        = $attempt->userid;
899         $tmpactivity->user->firstname = $attempt->firstname;
900         $tmpactivity->user->lastname  = $attempt->lastname;
901         $tmpactivity->user->fullname  = fullname($attempt, $viewfullnames);
902         $tmpactivity->user->picture   = $attempt->picture;
903         $tmpactivity->user->imagealt  = $attempt->imagealt;
904         $tmpactivity->user->email     = $attempt->email;
906         $activities[$index++] = $tmpactivity;
907     }
910 function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
911     global $CFG, $OUTPUT;
913     echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">';
915     echo '<tr><td class="userpicture" valign="top">';
916     echo $OUTPUT->user_picture($activity->user, array('courseid' => $courseid));
917     echo '</td><td>';
919     if ($detail) {
920         $modname = $modnames[$activity->type];
921         echo '<div class="title">';
922         echo '<img src="' . $OUTPUT->pix_url('icon', $activity->type) . '" ' .
923                 'class="icon" alt="' . $modname . '" />';
924         echo '<a href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
925                 $activity->cmid . '">' . $activity->name . '</a>';
926         echo '</div>';
927     }
929     echo '<div class="grade">';
930     echo  get_string('attempt', 'quiz', $activity->content->attempt);
931     if (isset($activity->content->maxgrade)) {
932         $grades = $activity->content->sumgrades . ' / ' . $activity->content->maxgrade;
933         echo ': (<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?attempt=' .
934                 $activity->content->attemptid . '">' . $grades . '</a>)';
935     }
936     echo '</div>';
938     echo '<div class="user">';
939     echo '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $activity->user->id .
940             '&amp;course=' . $courseid . '">' . $activity->user->fullname .
941             '</a> - ' . userdate($activity->timestamp);
942     echo '</div>';
944     echo '</td></tr></table>';
946     return;
949 /**
950  * Pre-process the quiz options form data, making any necessary adjustments.
951  * Called by add/update instance in this file.
952  *
953  * @param object $quiz The variables set on the form.
954  */
955 function quiz_process_options($quiz) {
956     global $CFG;
957     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
958     require_once($CFG->libdir . '/questionlib.php');
960     $quiz->timemodified = time();
962     // Quiz name.
963     if (!empty($quiz->name)) {
964         $quiz->name = trim($quiz->name);
965     }
967     // Password field - different in form to stop browsers that remember passwords
968     // getting confused.
969     $quiz->password = $quiz->quizpassword;
970     unset($quiz->quizpassword);
972     // Quiz feedback.
973     if (isset($quiz->feedbacktext)) {
974         // Clean up the boundary text.
975         for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
976             if (empty($quiz->feedbacktext[$i]['text'])) {
977                 $quiz->feedbacktext[$i]['text'] = '';
978             } else {
979                 $quiz->feedbacktext[$i]['text'] = trim($quiz->feedbacktext[$i]['text']);
980             }
981         }
983         // Check the boundary value is a number or a percentage, and in range.
984         $i = 0;
985         while (!empty($quiz->feedbackboundaries[$i])) {
986             $boundary = trim($quiz->feedbackboundaries[$i]);
987             if (!is_numeric($boundary)) {
988                 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
989                     $boundary = trim(substr($boundary, 0, -1));
990                     if (is_numeric($boundary)) {
991                         $boundary = $boundary * $quiz->grade / 100.0;
992                     } else {
993                         return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
994                     }
995                 }
996             }
997             if ($boundary <= 0 || $boundary >= $quiz->grade) {
998                 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
999             }
1000             if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
1001                 return get_string('feedbackerrororder', 'quiz', $i + 1);
1002             }
1003             $quiz->feedbackboundaries[$i] = $boundary;
1004             $i += 1;
1005         }
1006         $numboundaries = $i;
1008         // Check there is nothing in the remaining unused fields.
1009         if (!empty($quiz->feedbackboundaries)) {
1010             for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
1011                 if (!empty($quiz->feedbackboundaries[$i]) &&
1012                         trim($quiz->feedbackboundaries[$i]) != '') {
1013                     return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
1014                 }
1015             }
1016         }
1017         for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
1018             if (!empty($quiz->feedbacktext[$i]['text']) &&
1019                     trim($quiz->feedbacktext[$i]['text']) != '') {
1020                 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
1021             }
1022         }
1023         // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
1024         $quiz->feedbackboundaries[-1] = $quiz->grade + 1;
1025         $quiz->feedbackboundaries[$numboundaries] = 0;
1026         $quiz->feedbackboundarycount = $numboundaries;
1027     }
1029     // Combing the individual settings into the review columns.
1030     $quiz->reviewattempt = quiz_review_option_form_to_db($quiz, 'attempt');
1031     $quiz->reviewcorrectness = quiz_review_option_form_to_db($quiz, 'correctness');
1032     $quiz->reviewmarks = quiz_review_option_form_to_db($quiz, 'marks');
1033     $quiz->reviewspecificfeedback = quiz_review_option_form_to_db($quiz, 'specificfeedback');
1034     $quiz->reviewgeneralfeedback = quiz_review_option_form_to_db($quiz, 'generalfeedback');
1035     $quiz->reviewrightanswer = quiz_review_option_form_to_db($quiz, 'rightanswer');
1036     $quiz->reviewoverallfeedback = quiz_review_option_form_to_db($quiz, 'overallfeedback');
1037     $quiz->reviewattempt |= mod_quiz_display_options::DURING;
1038     $quiz->reviewoverallfeedback &= ~mod_quiz_display_options::DURING;
1041 /**
1042  * Helper function for {@link quiz_process_options()}.
1043  * @param object $fromform the sumbitted form date.
1044  * @param string $field one of the review option field names.
1045  */
1046 function quiz_review_option_form_to_db($fromform, $field) {
1047     static $times = array(
1048         'during' => mod_quiz_display_options::DURING,
1049         'immediately' => mod_quiz_display_options::IMMEDIATELY_AFTER,
1050         'open' => mod_quiz_display_options::LATER_WHILE_OPEN,
1051         'closed' => mod_quiz_display_options::AFTER_CLOSE,
1052     );
1054     $review = 0;
1055     foreach ($times as $whenname => $when) {
1056         $fieldname = $field . $whenname;
1057         if (isset($fromform->$fieldname)) {
1058             $review |= $when;
1059             unset($fromform->$fieldname);
1060         }
1061     }
1063     return $review;
1066 /**
1067  * This function is called at the end of quiz_add_instance
1068  * and quiz_update_instance, to do the common processing.
1069  *
1070  * @param object $quiz the quiz object.
1071  */
1072 function quiz_after_add_or_update($quiz) {
1073     global $DB;
1074     $cmid = $quiz->coursemodule;
1076     // We need to use context now, so we need to make sure all needed info is already in db.
1077     $DB->set_field('course_modules', 'instance', $quiz->id, array('id'=>$cmid));
1078     $context = get_context_instance(CONTEXT_MODULE, $cmid);
1080     // Save the feedback.
1081     $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
1083     for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) {
1084         $feedback = new stdClass();
1085         $feedback->quizid = $quiz->id;
1086         $feedback->feedbacktext = $quiz->feedbacktext[$i]['text'];
1087         $feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format'];
1088         $feedback->mingrade = $quiz->feedbackboundaries[$i];
1089         $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
1090         $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1091         $feedbacktext = file_save_draft_area_files((int)$quiz->feedbacktext[$i]['itemid'],
1092                 $context->id, 'mod_quiz', 'feedback', $feedback->id,
1093                 array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0),
1094                 $quiz->feedbacktext[$i]['text']);
1095         $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext,
1096                 array('id' => $feedback->id));
1097     }
1099     // Store any settings belonging to the access rules.
1100     quiz_access_manager::save_settings($quiz);
1102     // Update the events relating to this quiz.
1103     quiz_update_events($quiz);
1105     // Update related grade item.
1106     quiz_grade_item_update($quiz);
1109 /**
1110  * This function updates the events associated to the quiz.
1111  * If $override is non-zero, then it updates only the events
1112  * associated with the specified override.
1113  *
1114  * @uses QUIZ_MAX_EVENT_LENGTH
1115  * @param object $quiz the quiz object.
1116  * @param object optional $override limit to a specific override
1117  */
1118 function quiz_update_events($quiz, $override = null) {
1119     global $DB;
1121     // Load the old events relating to this quiz.
1122     $conds = array('modulename'=>'quiz',
1123                    'instance'=>$quiz->id);
1124     if (!empty($override)) {
1125         // Only load events for this override.
1126         $conds['groupid'] = isset($override->groupid)?  $override->groupid : 0;
1127         $conds['userid'] = isset($override->userid)?  $override->userid : 0;
1128     }
1129     $oldevents = $DB->get_records('event', $conds);
1131     // Now make a todo list of all that needs to be updated.
1132     if (empty($override)) {
1133         // We are updating the primary settings for the quiz, so we
1134         // need to add all the overrides.
1135         $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id));
1136         // As well as the original quiz (empty override).
1137         $overrides[] = new stdClass();
1138     } else {
1139         // Just do the one override.
1140         $overrides = array($override);
1141     }
1143     foreach ($overrides as $current) {
1144         $groupid   = isset($current->groupid)?  $current->groupid : 0;
1145         $userid    = isset($current->userid)? $current->userid : 0;
1146         $timeopen  = isset($current->timeopen)?  $current->timeopen : $quiz->timeopen;
1147         $timeclose = isset($current->timeclose)? $current->timeclose : $quiz->timeclose;
1149         // Only add open/close events for an override if they differ from the quiz default.
1150         $addopen  = empty($current->id) || !empty($current->timeopen);
1151         $addclose = empty($current->id) || !empty($current->timeclose);
1153         $event = new stdClass();
1154         $event->description = format_module_intro('quiz', $quiz, $quiz->coursemodule);
1155         // Events module won't show user events when the courseid is nonzero.
1156         $event->courseid    = ($userid) ? 0 : $quiz->course;
1157         $event->groupid     = $groupid;
1158         $event->userid      = $userid;
1159         $event->modulename  = 'quiz';
1160         $event->instance    = $quiz->id;
1161         $event->timestart   = $timeopen;
1162         $event->timeduration = max($timeclose - $timeopen, 0);
1163         $event->visible     = instance_is_visible('quiz', $quiz);
1164         $event->eventtype   = 'open';
1166         // Determine the event name.
1167         if ($groupid) {
1168             $params = new stdClass();
1169             $params->quiz = $quiz->name;
1170             $params->group = groups_get_group_name($groupid);
1171             if ($params->group === false) {
1172                 // Group doesn't exist, just skip it.
1173                 continue;
1174             }
1175             $eventname = get_string('overridegroupeventname', 'quiz', $params);
1176         } else if ($userid) {
1177             $params = new stdClass();
1178             $params->quiz = $quiz->name;
1179             $eventname = get_string('overrideusereventname', 'quiz', $params);
1180         } else {
1181             $eventname = $quiz->name;
1182         }
1183         if ($addopen or $addclose) {
1184             if ($timeclose and $timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) {
1185                 // Single event for the whole quiz.
1186                 if ($oldevent = array_shift($oldevents)) {
1187                     $event->id = $oldevent->id;
1188                 } else {
1189                     unset($event->id);
1190                 }
1191                 $event->name = $eventname;
1192                 // The method calendar_event::create will reuse a db record if the id field is set.
1193                 calendar_event::create($event);
1194             } else {
1195                 // Separate start and end events.
1196                 $event->timeduration  = 0;
1197                 if ($timeopen && $addopen) {
1198                     if ($oldevent = array_shift($oldevents)) {
1199                         $event->id = $oldevent->id;
1200                     } else {
1201                         unset($event->id);
1202                     }
1203                     $event->name = $eventname.' ('.get_string('quizopens', 'quiz').')';
1204                     // The method calendar_event::create will reuse a db record if the id field is set.
1205                     calendar_event::create($event);
1206                 }
1207                 if ($timeclose && $addclose) {
1208                     if ($oldevent = array_shift($oldevents)) {
1209                         $event->id = $oldevent->id;
1210                     } else {
1211                         unset($event->id);
1212                     }
1213                     $event->name      = $eventname.' ('.get_string('quizcloses', 'quiz').')';
1214                     $event->timestart = $timeclose;
1215                     $event->eventtype = 'close';
1216                     calendar_event::create($event);
1217                 }
1218             }
1219         }
1220     }
1222     // Delete any leftover events.
1223     foreach ($oldevents as $badevent) {
1224         $badevent = calendar_event::load($badevent);
1225         $badevent->delete();
1226     }
1229 /**
1230  * @return array
1231  */
1232 function quiz_get_view_actions() {
1233     return array('view', 'view all', 'report', 'review');
1236 /**
1237  * @return array
1238  */
1239 function quiz_get_post_actions() {
1240     return array('attempt', 'close attempt', 'preview', 'editquestions',
1241             'delete attempt', 'manualgrade');
1244 /**
1245  * @param array $questionids of question ids.
1246  * @return bool whether any of these questions are used by any instance of this module.
1247  */
1248 function quiz_questions_in_use($questionids) {
1249     global $DB, $CFG;
1250     require_once($CFG->libdir . '/questionlib.php');
1251     list($test, $params) = $DB->get_in_or_equal($questionids);
1252     return $DB->record_exists_select('quiz_question_instances',
1253             'question ' . $test, $params) || question_engine::questions_in_use(
1254             $questionids, new qubaid_join('{quiz_attempts} quiza',
1255             'quiza.uniqueid', 'quiza.preview = 0'));
1258 /**
1259  * Implementation of the function for printing the form elements that control
1260  * whether the course reset functionality affects the quiz.
1261  *
1262  * @param $mform the course reset form that is being built.
1263  */
1264 function quiz_reset_course_form_definition($mform) {
1265     $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz'));
1266     $mform->addElement('advcheckbox', 'reset_quiz_attempts',
1267             get_string('removeallquizattempts', 'quiz'));
1270 /**
1271  * Course reset form defaults.
1272  * @return array the defaults.
1273  */
1274 function quiz_reset_course_form_defaults($course) {
1275     return array('reset_quiz_attempts' => 1);
1278 /**
1279  * Removes all grades from gradebook
1280  *
1281  * @param int $courseid
1282  * @param string optional type
1283  */
1284 function quiz_reset_gradebook($courseid, $type='') {
1285     global $CFG, $DB;
1287     $quizzes = $DB->get_records_sql("
1288             SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
1289             FROM {modules} m
1290             JOIN {course_modules} cm ON m.id = cm.module
1291             JOIN {quiz} q ON cm.instance = q.id
1292             WHERE m.name = 'quiz' AND cm.course = ?", array($courseid));
1294     foreach ($quizzes as $quiz) {
1295         quiz_grade_item_update($quiz, 'reset');
1296     }
1299 /**
1300  * Actual implementation of the reset course functionality, delete all the
1301  * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is
1302  * set and true.
1303  *
1304  * Also, move the quiz open and close dates, if the course start date is changing.
1305  *
1306  * @param object $data the data submitted from the reset course.
1307  * @return array status array
1308  */
1309 function quiz_reset_userdata($data) {
1310     global $CFG, $DB;
1311     require_once($CFG->libdir.'/questionlib.php');
1313     $componentstr = get_string('modulenameplural', 'quiz');
1314     $status = array();
1316     // Delete attempts.
1317     if (!empty($data->reset_quiz_attempts)) {
1318         require_once($CFG->libdir . '/questionlib.php');
1320         question_engine::delete_questions_usage_by_activities(new qubaid_join(
1321                 '{quiz_attempts} quiza JOIN {quiz} quiz ON quiza.quiz = quiz.id',
1322                 'quiza.uniqueid', 'quiz.course = :quizcourseid',
1323                 array('quizcourseid' => $data->courseid)));
1325         $DB->delete_records_select('quiz_attempts',
1326                 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid));
1327         $status[] = array(
1328             'component' => $componentstr,
1329             'item' => get_string('attemptsdeleted', 'quiz'),
1330             'error' => false);
1332         // Remove all grades from gradebook.
1333         $DB->delete_records_select('quiz_grades',
1334                 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid));
1335         if (empty($data->reset_gradebook_grades)) {
1336             quiz_reset_gradebook($data->courseid);
1337         }
1338         $status[] = array(
1339             'component' => $componentstr,
1340             'item' => get_string('gradesdeleted', 'quiz'),
1341             'error' => false);
1342     }
1344     // Updating dates - shift may be negative too.
1345     if ($data->timeshift) {
1346         shift_course_mod_dates('quiz', array('timeopen', 'timeclose'),
1347                 $data->timeshift, $data->courseid);
1348         $status[] = array(
1349             'component' => $componentstr,
1350             'item' => get_string('openclosedatesupdated', 'quiz'),
1351             'error' => false);
1352     }
1354     return $status;
1357 /**
1358  * Checks whether the current user is allowed to view a file uploaded in a quiz.
1359  * Teachers can view any from their courses, students can only view their own.
1360  *
1361  * @param int $attemptuniqueid int attempt id
1362  * @param int $questionid int question id
1363  * @return bool to indicate access granted or denied
1364  */
1365 function quiz_check_file_access($attemptuniqueid, $questionid, $context = null) {
1366     global $USER, $DB, $CFG;
1367     require_once(dirname(__FILE__).'/attemptlib.php');
1368     require_once(dirname(__FILE__).'/locallib.php');
1370     $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $attemptuniqueid));
1371     $attemptobj = quiz_attempt::create($attempt->id);
1373     // Does the question exist?
1374     if (!$question = $DB->get_record('question', array('id' => $questionid))) {
1375         return false;
1376     }
1378     if ($context === null) {
1379         $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz));
1380         $cm = get_coursemodule_from_id('quiz', $quiz->id);
1381         $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1382     }
1384     // Load those questions and the associated states.
1385     $attemptobj->load_questions(array($questionid));
1386     $attemptobj->load_question_states(array($questionid));
1388     // Obtain the state.
1389     $state = $attemptobj->get_question_state($questionid);
1390     // Obtain the question.
1391     $question = $attemptobj->get_question($questionid);
1393     // Access granted if the current user submitted this file.
1394     if ($attempt->userid != $USER->id) {
1395         return false;
1396     }
1397     // Access granted if the current user has permission to grade quizzes in this course.
1398     if (!(has_capability('mod/quiz:viewreports', $context) ||
1399             has_capability('mod/quiz:grade', $context))) {
1400         return false;
1401     }
1403     return array($question, $state, array());
1406 /**
1407  * Prints quiz summaries on MyMoodle Page
1408  * @param arry $courses
1409  * @param array $htmlarray
1410  */
1411 function quiz_print_overview($courses, &$htmlarray) {
1412     global $USER, $CFG;
1413     // These next 6 Lines are constant in all modules (just change module name).
1414     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1415         return array();
1416     }
1418     if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
1419         return;
1420     }
1422     // Fetch some language strings outside the main loop.
1423     $strquiz = get_string('modulename', 'quiz');
1424     $strnoattempts = get_string('noattempts', 'quiz');
1426     // We want to list quizzes that are currently available, and which have a close date.
1427     // This is the same as what the lesson does, and the dabate is in MDL-10568.
1428     $now = time();
1429     foreach ($quizzes as $quiz) {
1430         if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
1431             // Give a link to the quiz, and the deadline.
1432             $str = '<div class="quiz overview">' .
1433                     '<div class="name">' . $strquiz . ': <a ' .
1434                     ($quiz->visible ? '' : ' class="dimmed"') .
1435                     ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
1436                     $quiz->coursemodule . '">' .
1437                     $quiz->name . '</a></div>';
1438             $str .= '<div class="info">' . get_string('quizcloseson', 'quiz',
1439                     userdate($quiz->timeclose)) . '</div>';
1441             // Now provide more information depending on the uers's role.
1442             $context = get_context_instance(CONTEXT_MODULE, $quiz->coursemodule);
1443             if (has_capability('mod/quiz:viewreports', $context)) {
1444                 // For teacher-like people, show a summary of the number of student attempts.
1445                 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
1446                 // fields set to make the following call work.
1447                 $str .= '<div class="info">' .
1448                         quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
1449             } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
1450                     $context)) { // Student
1451                 // For student-like people, tell them how many attempts they have made.
1452                 if (isset($USER->id) &&
1453                         ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
1454                     $numattempts = count($attempts);
1455                     $str .= '<div class="info">' .
1456                             get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
1457                 } else {
1458                     $str .= '<div class="info">' . $strnoattempts . '</div>';
1459                 }
1460             } else {
1461                 // For ayone else, there is no point listing this quiz, so stop processing.
1462                 continue;
1463             }
1465             // Add the output for this quiz to the rest.
1466             $str .= '</div>';
1467             if (empty($htmlarray[$quiz->course]['quiz'])) {
1468                 $htmlarray[$quiz->course]['quiz'] = $str;
1469             } else {
1470                 $htmlarray[$quiz->course]['quiz'] .= $str;
1471             }
1472         }
1473     }
1476 /**
1477  * Return a textual summary of the number of attempts that have been made at a particular quiz,
1478  * returns '' if no attempts have been made yet, unless $returnzero is passed as true.
1479  *
1480  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1481  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and
1482  *      $cm->groupingid fields are used at the moment.
1483  * @param bool $returnzero if false (default), when no attempts have been
1484  *      made '' is returned instead of 'Attempts: 0'.
1485  * @param int $currentgroup if there is a concept of current group where this method is being called
1486  *         (e.g. a report) pass it in here. Default 0 which means no current group.
1487  * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
1488  *          "Attemtps 123 (45 from this group)".
1489  */
1490 function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
1491     global $DB, $USER;
1492     $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0));
1493     if ($numattempts || $returnzero) {
1494         if (groups_get_activity_groupmode($cm)) {
1495             $a = new stdClass();
1496             $a->total = $numattempts;
1497             if ($currentgroup) {
1498                 $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' .
1499                         '{quiz_attempts} qa JOIN ' .
1500                         '{groups_members} gm ON qa.userid = gm.userid ' .
1501                         'WHERE quiz = ? AND preview = 0 AND groupid = ?',
1502                         array($quiz->id, $currentgroup));
1503                 return get_string('attemptsnumthisgroup', 'quiz', $a);
1504             } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
1505                 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups));
1506                 $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' .
1507                         '{quiz_attempts} qa JOIN ' .
1508                         '{groups_members} gm ON qa.userid = gm.userid ' .
1509                         'WHERE quiz = ? AND preview = 0 AND ' .
1510                         "groupid $usql", array_merge(array($quiz->id), $params));
1511                 return get_string('attemptsnumyourgroups', 'quiz', $a);
1512             }
1513         }
1514         return get_string('attemptsnum', 'quiz', $numattempts);
1515     }
1516     return '';
1519 /**
1520  * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
1521  * to the quiz reports.
1522  *
1523  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1524  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and
1525  *      $cm->groupingid fields are used at the moment.
1526  * @param object $context the quiz context.
1527  * @param bool $returnzero if false (default), when no attempts have been made
1528  *      '' is returned instead of 'Attempts: 0'.
1529  * @param int $currentgroup if there is a concept of current group where this method is being called
1530  *         (e.g. a report) pass it in here. Default 0 which means no current group.
1531  * @return string HTML fragment for the link.
1532  */
1533 function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false,
1534         $currentgroup = 0) {
1535     global $CFG;
1536     $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
1537     if (!$summary) {
1538         return '';
1539     }
1541     require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1542     $url = new moodle_url('/mod/quiz/report.php', array(
1543             'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
1544     return html_writer::link($url, $summary);
1547 /**
1548  * @param string $feature FEATURE_xx constant for requested feature
1549  * @return bool True if quiz supports feature
1550  */
1551 function quiz_supports($feature) {
1552     switch($feature) {
1553         case FEATURE_GROUPS:                  return true;
1554         case FEATURE_GROUPINGS:               return true;
1555         case FEATURE_GROUPMEMBERSONLY:        return true;
1556         case FEATURE_MOD_INTRO:               return true;
1557         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
1558         case FEATURE_GRADE_HAS_GRADE:         return true;
1559         case FEATURE_GRADE_OUTCOMES:          return false;
1560         case FEATURE_BACKUP_MOODLE2:          return true;
1561         case FEATURE_SHOW_DESCRIPTION:        return true;
1563         default: return null;
1564     }
1567 /**
1568  * @return array all other caps used in module
1569  */
1570 function quiz_get_extra_capabilities() {
1571     global $CFG;
1572     require_once($CFG->libdir.'/questionlib.php');
1573     $caps = question_get_all_capabilities();
1574     $caps[] = 'moodle/site:accessallgroups';
1575     return $caps;
1578 /**
1579  * This fucntion extends the global navigation for the site.
1580  * It is important to note that you should not rely on PAGE objects within this
1581  * body of code as there is no guarantee that during an AJAX request they are
1582  * available
1583  *
1584  * @param navigation_node $quiznode The quiz node within the global navigation
1585  * @param object $course The course object returned from the DB
1586  * @param object $module The module object returned from the DB
1587  * @param object $cm The course module instance returned from the DB
1588  */
1589 function quiz_extend_navigation($quiznode, $course, $module, $cm) {
1590     global $CFG;
1592     $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1594     if (has_capability('mod/quiz:view', $context)) {
1595         $url = new moodle_url('/mod/quiz/view.php', array('id'=>$cm->id));
1596         $quiznode->add(get_string('info', 'quiz'), $url, navigation_node::TYPE_SETTING,
1597                 null, null, new pix_icon('i/info', ''));
1598     }
1600     if (has_any_capability(array('mod/quiz:viewreports', 'mod/quiz:grade'), $context)) {
1601         require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php');
1602         $reportlist = quiz_report_list($context);
1604         $url = new moodle_url('/mod/quiz/report.php',
1605                 array('id' => $cm->id, 'mode' => reset($reportlist)));
1606         $reportnode = $quiznode->add(get_string('results', 'quiz'), $url,
1607                 navigation_node::TYPE_SETTING,
1608                 null, null, new pix_icon('i/report', ''));
1610         foreach ($reportlist as $report) {
1611             $url = new moodle_url('/mod/quiz/report.php',
1612                     array('id' => $cm->id, 'mode' => $report));
1613             $reportnode->add(get_string($report, 'quiz_'.$report), $url,
1614                     navigation_node::TYPE_SETTING,
1615                     null, 'quiz_report_' . $report, new pix_icon('i/item', ''));
1616         }
1617     }
1620 /**
1621  * This function extends the settings navigation block for the site.
1622  *
1623  * It is safe to rely on PAGE here as we will only ever be within the module
1624  * context when this is called
1625  *
1626  * @param settings_navigation $settings
1627  * @param navigation_node $quiznode
1628  */
1629 function quiz_extend_settings_navigation($settings, $quiznode) {
1630     global $PAGE, $CFG;
1632     // Require {@link questionlib.php}
1633     // Included here as we only ever want to include this file if we really need to.
1634     require_once($CFG->libdir . '/questionlib.php');
1636     // We want to add these new nodes after the Edit settings node, and before the
1637     // Locally assigned roles node. Of course, both of those are controlled by capabilities.
1638     $keys = $quiznode->get_children_key_list();
1639     $beforekey = null;
1640     $i = array_search('modedit', $keys);
1641     if ($i === false and array_key_exists(0, $keys)) {
1642         $beforekey = $keys[0];
1643     } else if (array_key_exists($i + 1, $keys)) {
1644         $beforekey = $keys[$i + 1];
1645     }
1647     if (has_capability('mod/quiz:manageoverrides', $PAGE->cm->context)) {
1648         $url = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$PAGE->cm->id));
1649         $node = navigation_node::create(get_string('groupoverrides', 'quiz'),
1650                 new moodle_url($url, array('mode'=>'group')),
1651                 navigation_node::TYPE_SETTING, null, 'mod_quiz_groupoverrides');
1652         $quiznode->add_node($node, $beforekey);
1654         $node = navigation_node::create(get_string('useroverrides', 'quiz'),
1655                 new moodle_url($url, array('mode'=>'user')),
1656                 navigation_node::TYPE_SETTING, null, 'mod_quiz_useroverrides');
1657         $quiznode->add_node($node, $beforekey);
1658     }
1660     if (has_capability('mod/quiz:manage', $PAGE->cm->context)) {
1661         $node = navigation_node::create(get_string('editquiz', 'quiz'),
1662                 new moodle_url('/mod/quiz/edit.php', array('cmid'=>$PAGE->cm->id)),
1663                 navigation_node::TYPE_SETTING, null, 'mod_quiz_edit',
1664                 new pix_icon('t/edit', ''));
1665         $quiznode->add_node($node, $beforekey);
1666     }
1668     if (has_capability('mod/quiz:preview', $PAGE->cm->context)) {
1669         $url = new moodle_url('/mod/quiz/startattempt.php',
1670                 array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey()));
1671         $node = navigation_node::create(get_string('preview', 'quiz'), $url,
1672                 navigation_node::TYPE_SETTING, null, 'mod_quiz_preview',
1673                 new pix_icon('t/preview', ''));
1674         $quiznode->add_node($node, $beforekey);
1675     }
1677     question_extend_settings_navigation($quiznode, $PAGE->cm->context)->trim_if_empty();
1680 /**
1681  * Serves the quiz files.
1682  *
1683  * @package  mod_quiz
1684  * @category files
1685  * @param stdClass $course course object
1686  * @param stdClass $cm course module object
1687  * @param stdClass $context context object
1688  * @param string $filearea file area
1689  * @param array $args extra arguments
1690  * @param bool $forcedownload whether or not force download
1691  * @param array $options additional options affecting the file serving
1692  * @return bool false if file not found, does not return if found - justsend the file
1693  */
1694 function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
1695     global $CFG, $DB;
1697     if ($context->contextlevel != CONTEXT_MODULE) {
1698         return false;
1699     }
1701     require_login($course, false, $cm);
1703     if (!$quiz = $DB->get_record('quiz', array('id'=>$cm->instance))) {
1704         return false;
1705     }
1707     // The 'intro' area is served by pluginfile.php.
1708     $fileareas = array('feedback');
1709     if (!in_array($filearea, $fileareas)) {
1710         return false;
1711     }
1713     $feedbackid = (int)array_shift($args);
1714     if (!$feedback = $DB->get_record('quiz_feedback', array('id'=>$feedbackid))) {
1715         return false;
1716     }
1718     $fs = get_file_storage();
1719     $relativepath = implode('/', $args);
1720     $fullpath = "/$context->id/mod_quiz/$filearea/$feedbackid/$relativepath";
1721     if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1722         return false;
1723     }
1724     send_stored_file($file, 0, 0, true, $options);
1727 /**
1728  * Called via pluginfile.php -> question_pluginfile to serve files belonging to
1729  * a question in a question_attempt when that attempt is a quiz attempt.
1730  *
1731  * @package  mod_quiz
1732  * @category files
1733  * @param stdClass $course course settings object
1734  * @param stdClass $context context object
1735  * @param string $component the name of the component we are serving files for.
1736  * @param string $filearea the name of the file area.
1737  * @param int $qubaid the attempt usage id.
1738  * @param int $slot the id of a question in this quiz attempt.
1739  * @param array $args the remaining bits of the file path.
1740  * @param bool $forcedownload whether the user must be forced to download the file.
1741  * @param array $options additional options affecting the file serving
1742  * @return bool false if file not found, does not return if found - justsend the file
1743  */
1744 function quiz_question_pluginfile($course, $context, $component,
1745         $filearea, $qubaid, $slot, $args, $forcedownload, array $options=array()) {
1746     global $CFG;
1747     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
1749     $attemptobj = quiz_attempt::create_from_usage_id($qubaid);
1750     require_login($attemptobj->get_course(), false, $attemptobj->get_cm());
1752     if ($attemptobj->is_own_attempt() && !$attemptobj->is_finished()) {
1753         // In the middle of an attempt.
1754         if (!$attemptobj->is_preview_user()) {
1755             $attemptobj->require_capability('mod/quiz:attempt');
1756         }
1757         $isreviewing = false;
1759     } else {
1760         // Reviewing an attempt.
1761         $attemptobj->check_review_capability();
1762         $isreviewing = true;
1763     }
1765     if (!$attemptobj->check_file_access($slot, $isreviewing, $context->id,
1766             $component, $filearea, $args, $forcedownload)) {
1767         send_file_not_found();
1768     }
1770     $fs = get_file_storage();
1771     $relativepath = implode('/', $args);
1772     $fullpath = "/$context->id/$component/$filearea/$relativepath";
1773     if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1774         send_file_not_found();
1775     }
1777     send_stored_file($file, 0, 0, $forcedownload, $options);
1780 /**
1781  * Return a list of page types
1782  * @param string $pagetype current page type
1783  * @param stdClass $parentcontext Block's parent context
1784  * @param stdClass $currentcontext Current context of block
1785  */
1786 function quiz_page_type_list($pagetype, $parentcontext, $currentcontext) {
1787     $module_pagetype = array(
1788         'mod-quiz-*'=>get_string('page-mod-quiz-x', 'quiz'),
1789         'mod-quiz-edit'=>get_string('page-mod-quiz-edit', 'quiz'));
1790     return $module_pagetype;
1793 /**
1794  * @return the options for quiz navigation.
1795  */
1796 function quiz_get_navigation_options() {
1797     return array(
1798         QUIZ_NAVMETHOD_FREE => get_string('navmethod_free', 'quiz'),
1799         QUIZ_NAVMETHOD_SEQ  => get_string('navmethod_seq', 'quiz')
1800     );