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