MDL-25316 quiz_get_extra_capabilities() does not need to fetch subcapabilities explicitly
[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";
607 if ($rs = $DB->get_recordset_sql($sql)) {
775f811a 608 $pbar = new progress_bar('quizupgradegrades', 500, true);
609 $i=0;
610 foreach ($rs as $quiz) {
611 $i++;
612 upgrade_set_timeout(60*5); // set up timeout, may also abort execution
613 quiz_update_grades($quiz, 0, false);
614 $pbar->update($i, $count, "Updating Quiz grades ($i/$count).");
d6dd2108 615 }
775f811a 616 $rs->close();
d6dd2108 617 }
d0ac6bc2 618}
619
d6dd2108 620/**
621 * Create grade item for given quiz
622 *
8cc86111 623 * @global stdClass
624 * @uses GRADE_TYPE_VALUE
625 * @uses GRADE_TYPE_NONE
626 * @uses QUIZ_REVIEW_SCORES
627 * @uses QUIZ_REVIEW_CLOSED
628 * @uses QUIZ_REVIEW_OPEN
629 * @uses PARAM_INT
630 * @uses GRADE_UPDATE_ITEM_LOCKED
d6dd2108 631 * @param object $quiz object with extra cmidnumber
8cc86111 632 * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
d6dd2108 633 * @return int 0 if ok, error code otherwise
634 */
ced5ee59 635function quiz_grade_item_update($quiz, $grades=NULL) {
3b1d5cc4 636 global $CFG, $OUTPUT;
d6dd2108 637 if (!function_exists('grade_update')) { //workaround for buggy PHP versions
638 require_once($CFG->libdir.'/gradelib.php');
639 }
640
641 if (array_key_exists('cmidnumber', $quiz)) { //it may not be always present
642 $params = array('itemname'=>$quiz->name, 'idnumber'=>$quiz->cmidnumber);
643 } else {
644 $params = array('itemname'=>$quiz->name);
645 }
646
647 if ($quiz->grade > 0) {
648 $params['gradetype'] = GRADE_TYPE_VALUE;
649 $params['grademax'] = $quiz->grade;
650 $params['grademin'] = 0;
651
652 } else {
653 $params['gradetype'] = GRADE_TYPE_NONE;
654 }
655
1223d24a 656/* description by TJ:
6571/ If the quiz is set to not show scores while the quiz is still open, and is set to show scores after
658 the quiz is closed, then create the grade_item with a show-after date that is the quiz close date.
6592/ If the quiz is set to not show scores at either of those times, create the grade_item as hidden.
6603/ If the quiz is set to show scores, create the grade_item visible.
661*/
662 if (!($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
663 and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
664 $params['hidden'] = 1;
665
666 } else if ( ($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
667 and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
668 if ($quiz->timeclose) {
669 $params['hidden'] = $quiz->timeclose;
670 } else {
671 $params['hidden'] = 1;
672 }
673
674 } else {
675 // a) both open and closed enabled
676 // b) open enabled, closed disabled - we can not "hide after", grades are kept visible even after closing
677 $params['hidden'] = 0;
678 }
679
0b5a80a1 680 if ($grades === 'reset') {
681 $params['reset'] = true;
682 $grades = NULL;
683 }
9cf4a18b 684
49460d84 685 $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
d45459b7 686 if (!empty($gradebook_grades->items)) {
687 $grade_item = $gradebook_grades->items[0];
688 if ($grade_item->locked) {
689 $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
690 if (!$confirm_regrade) {
691 $message = get_string('gradeitemislocked', 'grades');
692 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id . '&amp;mode=overview';
693 $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
3b1d5cc4 694 echo $OUTPUT->box_start('generalbox', 'notice');
d45459b7 695 echo '<p>'. $message .'</p>';
39e37019 696 echo $OUTPUT->container_start('buttons');
5c2ed7e2
PS
697 echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades'));
698 echo $OUTPUT->single_button($back_link, get_string('cancel'));
39e37019 699 echo $OUTPUT->container_end();
3b1d5cc4 700 echo $OUTPUT->box_end();
9cf4a18b 701
d45459b7 702 return GRADE_UPDATE_ITEM_LOCKED;
703 }
49460d84 704 }
705 }
0b5a80a1 706
ced5ee59 707 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
d6dd2108 708}
709
710/**
711 * Delete grade item for given quiz
712 *
8cc86111 713 * @global stdClass
d6dd2108 714 * @param object $quiz object
715 * @return object quiz
716 */
717function quiz_grade_item_delete($quiz) {
718 global $CFG;
53004e48 719 require_once($CFG->libdir . '/gradelib.php');
d6dd2108 720
53004e48 721 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, NULL, array('deleted' => 1));
d6dd2108 722}
723
e2249afe 724/**
725 * @return the options for calculating the quiz grade from the individual attempt grades.
726 */
727function quiz_get_grading_options() {
728 return array (
729 QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'),
730 QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'),
731 QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'),
732 QUIZ_ATTEMPTLAST => get_string('attemptlast', 'quiz'));
733}
d6dd2108 734
8cc86111 735/**
736 * Returns an array of users who have data in a given quiz
737 *
738 * @global stdClass
739 * @global object
740 * @param int $quizid
741 * @return array
742 */
d061d883 743function quiz_get_participants($quizid) {
9cf4a18b 744 global $CFG, $DB;
d061d883 745
e4acc4ce 746 //Get users from attempts
9cf4a18b 747 $us_attempts = $DB->get_records_sql("SELECT DISTINCT u.id, u.id
748 FROM {user} u,
749 {quiz_attempts} a
750 WHERE a.quiz = ? and
751 u.id = a.userid", array($quizid));
e4acc4ce 752
e4acc4ce 753 //Return us_attempts array (it contains an array of unique users)
6b224376 754 return $us_attempts;
e4acc4ce 755
d061d883 756}
730fd187 757
8cc86111 758/**
759 * This standard function will check all instances of this module
760 * and make sure there are up-to-date events created for each of them.
761 * If courseid = 0, then every quiz event in the site is checked, else
762 * only quiz events belonging to the course specified are checked.
763 * This function is used, in its new format, by restore_refresh_events()
764 *
765 * @global object
766 * @uses QUIZ_MAX_EVENT_LENGTH
767 * @param int $courseid
768 * @return bool
769 */
d2f308c0 770function quiz_refresh_events($courseid = 0) {
9cf4a18b 771 global $DB;
d2f308c0 772
773 if ($courseid == 0) {
9cf4a18b 774 if (! $quizzes = $DB->get_records('quiz')) {
d2f308c0 775 return true;
776 }
777 } else {
9cf4a18b 778 if (! $quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
d2f308c0 779 return true;
780 }
781 }
f41e824f 782
d2f308c0 783 foreach ($quizzes as $quiz) {
990650f9 784 quiz_update_events($quiz);
d2f308c0 785 }
990650f9 786
d2f308c0 787 return true;
788}
789
dd97c328 790/**
791 * Returns all quiz graded users since a given time for specified quiz
792 */
8d297188
TH
793function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
794 $courseid, $cmid, $userid = 0, $groupid = 0) {
9cf4a18b 795 global $CFG, $COURSE, $USER, $DB;
8d297188 796 require_once('locallib.php');
6710ec87 797
dd97c328 798 if ($COURSE->id == $courseid) {
799 $course = $COURSE;
6710ec87 800 } else {
9cf4a18b 801 $course = $DB->get_record('course', array('id' => $courseid));
6710ec87 802 }
6710ec87 803
dd97c328 804 $modinfo =& get_fast_modinfo($course);
6710ec87 805
dd97c328 806 $cm = $modinfo->cms[$cmid];
8d297188 807 $quiz = $DB->get_record('quiz', array('id' => $cm->instance));
9cf4a18b 808
dd97c328 809 if ($userid) {
8d297188
TH
810 $userselect = "AND u.id = :userid";
811 $params['userid'] = $userid;
dd97c328 812 } else {
8d297188 813 $userselect = '';
dd97c328 814 }
ac21ad39 815
dd97c328 816 if ($groupid) {
8d297188
TH
817 $groupselect = 'AND gm.groupid = :groupid';
818 $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id';
819 $params['groupid'] = $groupid;
dd97c328 820 } else {
8d297188
TH
821 $groupselect = '';
822 $groupjoin = '';
823 }
824
122fc5d9
DM
825 $params['timestart'] = $timestart;
826 $params['quizid'] = $quiz->id;
827
8d297188
TH
828 if (!$attempts = $DB->get_records_sql("
829 SELECT qa.*,
830 u.firstname, u.lastname, u.email, u.picture, u.imagealt
831 FROM {quiz_attempts} qa
832 JOIN {user} u ON u.id = qa.userid
833 $groupjoin
834 WHERE qa.timefinish > :timestart
835 AND qa.quiz = :quizid
836 AND qa.preview = 0
837 $userselect
838 $groupselect
839 ORDER BY qa.timefinish ASC", $params)) {
840 return;
841 }
842
843 $context = get_context_instance(CONTEXT_MODULE, $cm->id);
8d297188
TH
844 $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
845 $viewfullnames = has_capability('moodle/site:viewfullnames', $context);
cb323d02 846 $grader = has_capability('mod/quiz:viewreports', $context);
dd97c328 847 $groupmode = groups_get_activity_groupmode($cm, $course);
6710ec87 848
dd97c328 849 if (is_null($modinfo->groups)) {
850 $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
851 }
6710ec87 852
8d297188 853 $usersgroups = null;
dd97c328 854 $aname = format_string($cm->name,true);
855 foreach ($attempts as $attempt) {
856 if ($attempt->userid != $USER->id) {
857 if (!$grader) {
8d297188 858 // Grade permission required
dd97c328 859 continue;
860 }
6710ec87 861
9cf4a18b 862 if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
8d297188
TH
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 }
dd97c328 871 }
8d297188 872 if (!array_intersect($usersgroups, $modinfo->groups[$cm->id])) {
dd97c328 873 continue;
874 }
875 }
8d297188
TH
876 }
877
878 $options = quiz_get_reviewoptions($quiz, $attempt, $context);
dd97c328 879
8d297188 880 $tmpactivity = new stdClass;
dd97c328 881
8d297188
TH
882 $tmpactivity->type = 'quiz';
883 $tmpactivity->cmid = $cm->id;
884 $tmpactivity->name = $aname;
885 $tmpactivity->sectionnum = $cm->sectionnum;
886 $tmpactivity->timestamp = $attempt->timefinish;
9cf4a18b 887
dd97c328 888 $tmpactivity->content->attemptid = $attempt->id;
dd97c328 889 $tmpactivity->content->attempt = $attempt->attempt;
8d297188
TH
890 if (quiz_has_grades($quiz) && $options->scores) {
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 }
9cf4a18b 897
2a27a37d 898 $tmpactivity->user->id = $attempt->userid;
8d297188
TH
899 $tmpactivity->user->firstname = $attempt->firstname;
900 $tmpactivity->user->lastname = $attempt->lastname;
dd97c328 901 $tmpactivity->user->fullname = fullname($attempt, $viewfullnames);
902 $tmpactivity->user->picture = $attempt->picture;
8d297188 903 $tmpactivity->user->imagealt = $attempt->imagealt;
3a11c09f 904 $tmpactivity->user->email = $attempt->email;
9cf4a18b 905
dd97c328 906 $activities[$index++] = $tmpactivity;
6710ec87 907 }
908
909 return;
910}
911
dd97c328 912function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
e63f88c9 913 global $CFG, $OUTPUT;
6710ec87 914
dd97c328 915 echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">';
6710ec87 916
8d297188
TH
917 echo '<tr><td class="userpicture" valign="top">';
918 echo $OUTPUT->user_picture($activity->user, array('courseid' => $courseid));
919 echo '</td><td>';
6710ec87 920
921 if ($detail) {
dd97c328 922 $modname = $modnames[$activity->type];
923 echo '<div class="title">';
8d297188
TH
924 echo '<img src="' . $OUTPUT->pix_url('icon', $activity->type) . '" ' .
925 'class="icon" alt="' . $modname . '" />';
926 echo '<a href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
927 $activity->cmid . '">' . $activity->name . '</a>';
dd97c328 928 echo '</div>';
6710ec87 929 }
930
dd97c328 931 echo '<div class="grade">';
8d297188
TH
932 echo get_string('attempt', 'quiz', $activity->content->attempt);
933 if (isset($activity->content->maxgrade)) {
934 $grades = $activity->content->sumgrades . ' / ' . $activity->content->maxgrade;
935 echo ': (<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?attempt=' .
936 $activity->content->attemptid . '">' . $grades . '</a>)';
937 }
dd97c328 938 echo '</div>';
6710ec87 939
dd97c328 940 echo '<div class="user">';
8d297188
TH
941 echo '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $activity->user->id .
942 '&amp;course=' . $courseid . '">' . $activity->user->fullname .
943 '</a> - ' . userdate($activity->timestamp);
dd97c328 944 echo '</div>';
6710ec87 945
8d297188 946 echo '</td></tr></table>';
6710ec87 947
948 return;
949}
950
ee1fb969 951/**
920b93d1 952 * Pre-process the quiz options form data, making any necessary adjustments.
ad4cd837 953 * Called by add/update instance in this file.
b159da78 954 *
8cc86111 955 * @uses QUIZ_REVIEW_OVERALLFEEDBACK
956 * @uses QUIZ_REVIEW_CLOSED
957 * @uses QUIZ_REVIEW_OPEN
958 * @uses QUIZ_REVIEW_IMMEDIATELY
959 * @uses QUIZ_REVIEW_GENERALFEEDBACK
960 * @uses QUIZ_REVIEW_SOLUTIONS
961 * @uses QUIZ_REVIEW_ANSWERS
962 * @uses QUIZ_REVIEW_FEEDBACK
963 * @uses QUIZ_REVIEW_SCORES
964 * @uses QUIZ_REVIEW_RESPONSES
965 * @uses QUESTION_ADAPTIVE
920b93d1 966 * @param object $quiz The variables set on the form.
8cc86111 967 * @return string
920b93d1 968 */
969function quiz_process_options(&$quiz) {
970 $quiz->timemodified = time();
ee1fb969 971
dc5c6851 972 // Quiz name.
973 if (!empty($quiz->name)) {
974 $quiz->name = trim($quiz->name);
975 }
a23f0aaf 976
ab0a8ff2 977 // Password field - different in form to stop browsers that remember passwords
978 // getting confused.
979 $quiz->password = $quiz->quizpassword;
980 unset($quiz->quizpassword);
981
212b7b8c 982 // Quiz feedback
a0807a00 983 if (isset($quiz->feedbacktext)) {
984 // Clean up the boundary text.
985 for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
fe6ce234
DC
986 if (empty($quiz->feedbacktext[$i]['text'])) {
987 $quiz->feedbacktext[$i]['text'] = '';
a0807a00 988 } else {
fe6ce234 989 $quiz->feedbacktext[$i]['text'] = trim($quiz->feedbacktext[$i]['text']);
a0807a00 990 }
212b7b8c 991 }
b159da78 992
a0807a00 993 // Check the boundary value is a number or a percentage, and in range.
994 $i = 0;
995 while (!empty($quiz->feedbackboundaries[$i])) {
996 $boundary = trim($quiz->feedbackboundaries[$i]);
997 if (!is_numeric($boundary)) {
998 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
999 $boundary = trim(substr($boundary, 0, -1));
1000 if (is_numeric($boundary)) {
1001 $boundary = $boundary * $quiz->grade / 100.0;
1002 } else {
1003 return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
1004 }
212b7b8c 1005 }
1006 }
a0807a00 1007 if ($boundary <= 0 || $boundary >= $quiz->grade) {
1008 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
1009 }
1010 if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
1011 return get_string('feedbackerrororder', 'quiz', $i + 1);
1012 }
1013 $quiz->feedbackboundaries[$i] = $boundary;
1014 $i += 1;
212b7b8c 1015 }
a0807a00 1016 $numboundaries = $i;
b159da78 1017
a0807a00 1018 // Check there is nothing in the remaining unused fields.
e0b7cfcb 1019 if (!empty($quiz->feedbackboundaries)) {
1020 for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
1021 if (!empty($quiz->feedbackboundaries[$i]) && trim($quiz->feedbackboundaries[$i]) != '') {
1022 return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
1023 }
a0807a00 1024 }
212b7b8c 1025 }
a0807a00 1026 for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
fe6ce234 1027 if (!empty($quiz->feedbacktext[$i]['text']) && trim($quiz->feedbacktext[$i]['text']) != '') {
a0807a00 1028 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
1029 }
212b7b8c 1030 }
a0807a00 1031 $quiz->feedbackboundaries[-1] = $quiz->grade + 1; // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
1032 $quiz->feedbackboundaries[$numboundaries] = 0;
1033 $quiz->feedbackboundarycount = $numboundaries;
212b7b8c 1034 }
a23f0aaf 1035
920b93d1 1036 // Settings that get combined to go into the optionflags column.
1037 $quiz->optionflags = 0;
1038 if (!empty($quiz->adaptive)) {
1039 $quiz->optionflags |= QUESTION_ADAPTIVE;
1040 }
1041
1042 // Settings that get combined to go into the review column.
1043 $review = 0;
1044 if (isset($quiz->responsesimmediately)) {
ee1fb969 1045 $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 1046 unset($quiz->responsesimmediately);
ee1fb969 1047 }
920b93d1 1048 if (isset($quiz->responsesopen)) {
ee1fb969 1049 $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN);
920b93d1 1050 unset($quiz->responsesopen);
ee1fb969 1051 }
920b93d1 1052 if (isset($quiz->responsesclosed)) {
ee1fb969 1053 $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED);
920b93d1 1054 unset($quiz->responsesclosed);
ee1fb969 1055 }
1056
920b93d1 1057 if (isset($quiz->scoreimmediately)) {
ee1fb969 1058 $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 1059 unset($quiz->scoreimmediately);
ee1fb969 1060 }
920b93d1 1061 if (isset($quiz->scoreopen)) {
ee1fb969 1062 $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN);
920b93d1 1063 unset($quiz->scoreopen);
ee1fb969 1064 }
920b93d1 1065 if (isset($quiz->scoreclosed)) {
ee1fb969 1066 $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED);
920b93d1 1067 unset($quiz->scoreclosed);
ee1fb969 1068 }
1069
920b93d1 1070 if (isset($quiz->feedbackimmediately)) {
ee1fb969 1071 $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 1072 unset($quiz->feedbackimmediately);
ee1fb969 1073 }
920b93d1 1074 if (isset($quiz->feedbackopen)) {
ee1fb969 1075 $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN);
920b93d1 1076 unset($quiz->feedbackopen);
ee1fb969 1077 }
920b93d1 1078 if (isset($quiz->feedbackclosed)) {
ee1fb969 1079 $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED);
920b93d1 1080 unset($quiz->feedbackclosed);
ee1fb969 1081 }
1082
920b93d1 1083 if (isset($quiz->answersimmediately)) {
ee1fb969 1084 $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 1085 unset($quiz->answersimmediately);
ee1fb969 1086 }
920b93d1 1087 if (isset($quiz->answersopen)) {
ee1fb969 1088 $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN);
920b93d1 1089 unset($quiz->answersopen);
ee1fb969 1090 }
920b93d1 1091 if (isset($quiz->answersclosed)) {
ee1fb969 1092 $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED);
920b93d1 1093 unset($quiz->answersclosed);
ee1fb969 1094 }
1095
920b93d1 1096 if (isset($quiz->solutionsimmediately)) {
ee1fb969 1097 $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 1098 unset($quiz->solutionsimmediately);
ee1fb969 1099 }
920b93d1 1100 if (isset($quiz->solutionsopen)) {
ee1fb969 1101 $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_OPEN);
920b93d1 1102 unset($quiz->solutionsopen);
ee1fb969 1103 }
920b93d1 1104 if (isset($quiz->solutionsclosed)) {
ee1fb969 1105 $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_CLOSED);
920b93d1 1106 unset($quiz->solutionsclosed);
ee1fb969 1107 }
1108
a4514d91 1109 if (isset($quiz->generalfeedbackimmediately)) {
1110 $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
00719c02 1111 unset($quiz->generalfeedbackimmediately);
1b8a7434 1112 }
a4514d91 1113 if (isset($quiz->generalfeedbackopen)) {
1114 $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_OPEN);
00719c02 1115 unset($quiz->generalfeedbackopen);
1b8a7434 1116 }
a4514d91 1117 if (isset($quiz->generalfeedbackclosed)) {
1118 $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_CLOSED);
00719c02 1119 unset($quiz->generalfeedbackclosed);
1120 }
1121
1122 if (isset($quiz->overallfeedbackimmediately)) {
1123 $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1124 unset($quiz->overallfeedbackimmediately);
1125 }
1126 if (isset($quiz->overallfeedbackopen)) {
1127 $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_OPEN);
1128 unset($quiz->overallfeedbackopen);
1129 }
1130 if (isset($quiz->overallfeedbackclosed)) {
1131 $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_CLOSED);
1132 unset($quiz->overallfeedbackclosed);
1b8a7434 1133 }
1134
920b93d1 1135 $quiz->review = $review;
1136}
1137
1138/**
1139 * This function is called at the end of quiz_add_instance
1140 * and quiz_update_instance, to do the common processing.
a23f0aaf 1141 *
8cc86111 1142 * @global object
1143 * @uses QUIZ_MAX_EVENT_LENGTH
920b93d1 1144 * @param object $quiz the quiz object.
8cc86111 1145 * @return void|string Void or error message
920b93d1 1146 */
1147function quiz_after_add_or_update($quiz) {
c18269c7 1148 global $DB;
fe6ce234
DC
1149 $cmid = $quiz->coursemodule;
1150
1151 // we need to use context now, so we need to make sure all needed info is already in db
1152 $DB->set_field('course_modules', 'instance', $quiz->id, array('id'=>$cmid));
1153 $context = get_context_instance(CONTEXT_MODULE, $cmid);
920b93d1 1154
212b7b8c 1155 // Save the feedback
53004e48 1156 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
a23f0aaf 1157
fe6ce234 1158 for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) {
212b7b8c 1159 $feedback = new stdClass;
1160 $feedback->quizid = $quiz->id;
fe6ce234
DC
1161 $feedback->feedbacktext = $quiz->feedbacktext[$i]['text'];
1162 $feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format'];
212b7b8c 1163 $feedback->mingrade = $quiz->feedbackboundaries[$i];
1164 $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
fe6ce234
DC
1165 $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1166 $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']);
1167 $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext, array('id'=>$feedback->id));
212b7b8c 1168 }
1169
920b93d1 1170 // Update the events relating to this quiz.
990650f9
TH
1171 quiz_update_events($quiz);
1172
1173 //update related grade item
1174 quiz_grade_item_update($quiz);
1175
1176}
1177
1178/**
1179 * This function updates the events associated to the quiz.
1180 * If $override is non-zero, then it updates only the events
1181 * associated with the specified override.
1182 *
1183 * @uses QUIZ_MAX_EVENT_LENGTH
1184 * @param object $quiz the quiz object.
1185 * @param object optional $override limit to a specific override
1186 */
1187function quiz_update_events($quiz, $override = null) {
1188 global $DB;
1189
1190 // Load the old events relating to this quiz.
1191 $conds = array('modulename'=>'quiz',
1192 'instance'=>$quiz->id);
1193 if (!empty($override)) {
1194 // only load events for this override
1195 $conds['groupid'] = isset($override->groupid)? $override->groupid : 0;
1196 $conds['userid'] = isset($override->userid)? $override->userid : 0;
1197 }
1198 $oldevents = $DB->get_records('event', $conds);
1199
1200 // Now make a todo list of all that needs to be updated
1201 if (empty($override)) {
1202 // We are updating the primary settings for the quiz, so we
1203 // need to add all the overrides
1204 $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id));
1205 // as well as the original quiz (empty override)
1206 $overrides[] = new stdClass;
1207 }
1208 else {
1209 // Just do the one override
1210 $overrides = array($override);
920b93d1 1211 }
1212
990650f9
TH
1213 foreach ($overrides as $current) {
1214 $groupid = isset($current->groupid)? $current->groupid : 0;
1215 $userid = isset($current->userid)? $current->userid : 0;
1216 $timeopen = isset($current->timeopen)? $current->timeopen : $quiz->timeopen;
1217 $timeclose = isset($current->timeclose)? $current->timeclose : $quiz->timeclose;
1218
1219 // only add open/close events for an override if they differ from the quiz default
1220 $addopen = empty($current->id) || !empty($current->timeopen);
1221 $addclose = empty($current->id) || !empty($current->timeclose);
1222
1223 $event = new stdClass;
1224 $event->description = $quiz->intro;
1225 $event->courseid = ($userid) ? 0 : $quiz->course; // Events module won't show user events when the courseid is nonzero
1226 $event->groupid = $groupid;
1227 $event->userid = $userid;
1228 $event->modulename = 'quiz';
1229 $event->instance = $quiz->id;
1230 $event->timestart = $timeopen;
1231 $event->timeduration = max($timeclose - $timeopen, 0);
1232 $event->visible = instance_is_visible('quiz', $quiz);
1233 $event->eventtype = 'open';
1234
1235 // Determine the event name
1236 if ($groupid) {
1237 $params = new stdClass;
1238 $params->quiz = $quiz->name;
1239 $params->group = groups_get_group_name($groupid);
1240 if ($params->group === false) {
1241 // group doesn't exist, just skip it
1242 continue;
1243 }
1244 $eventname = get_string('overridegroupeventname', 'quiz', $params);
920b93d1 1245 }
990650f9
TH
1246 else if ($userid) {
1247 $params = new stdClass;
1248 $params->quiz = $quiz->name;
1249 $eventname = get_string('overrideusereventname', 'quiz', $params);
1250 } else {
1251 $eventname = $quiz->name;
1252 }
1253 if ($addopen or $addclose) {
1254 if ($timeclose and $timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) {
1255 // Single event for the whole quiz.
1256 if ($oldevent = array_shift($oldevents)) {
1257 $event->id = $oldevent->id;
1258 }
1259 else {
1260 unset($event->id);
1261 }
1262 $event->name = $eventname;
1263 // calendar_event::create will reuse a db record if the id field is set
1264 calendar_event::create($event);
1265 } else {
1266 // Separate start and end events.
1267 $event->timeduration = 0;
1268 if ($timeopen && $addopen) {
1269 if ($oldevent = array_shift($oldevents)) {
1270 $event->id = $oldevent->id;
1271 }
1272 else {
1273 unset($event->id);
1274 }
1275 $event->name = $eventname.' ('.get_string('quizopens', 'quiz').')';
1276 // calendar_event::create will reuse a db record if the id field is set
1277 calendar_event::create($event);
1278 }
1279 if ($timeclose && $addclose) {
1280 if ($oldevent = array_shift($oldevents)) {
1281 $event->id = $oldevent->id;
1282 }
1283 else {
1284 unset($event->id);
1285 }
1286 $event->name = $eventname.' ('.get_string('quizcloses', 'quiz').')';
1287 $event->timestart = $timeclose;
1288 $event->eventtype = 'close';
1289 calendar_event::create($event);
1290 }
1291 }
920b93d1 1292 }
1293 }
d6dd2108 1294
990650f9
TH
1295 // Delete any leftover events
1296 foreach ($oldevents as $badevent) {
1297 $badevent = calendar_event::load($badevent);
1298 $badevent->delete();
1299 }
ee1fb969 1300}
1301
8cc86111 1302/**
1303 * @return array
1304 */
f3221af9 1305function quiz_get_view_actions() {
acf149ad 1306 return array('view', 'view all', 'report', 'review');
f3221af9 1307}
ee1fb969 1308
8cc86111 1309/**
1310 * @return array
1311 */
f3221af9 1312function quiz_get_post_actions() {
acf149ad 1313 return array('attempt', 'close attempt', 'preview', 'editquestions', 'delete attempt', 'manualgrade');
f3221af9 1314}
ee1fb969 1315
f67172b6 1316/**
1317 * Returns an array of names of quizzes that use this question
1318 *
64d79492 1319 * @param integer $questionid
f67172b6 1320 * @return array of strings
1321 */
1322function quiz_question_list_instances($questionid) {
c6307ef2 1323 global $CFG, $DB;
e8666d9a 1324
64d79492 1325 // TODO MDL-5780: we should also consider other questions that are used by
e8666d9a 1326 // random questions in this quiz, but that is very hard.
1327
1328 $sql = "SELECT q.id, q.name
c6307ef2 1329 FROM {quiz} q
1330 JOIN {quiz_question_instances} qqi ON q.id = qqi.quiz
1331 WHERE qqi.question = ?";
e8666d9a 1332
c6307ef2 1333 if ($instances = $DB->get_records_sql_menu($sql, array($questionid))) {
e8666d9a 1334 return $instances;
1335 }
f67172b6 1336 return array();
1337}
1338
7a6f4066 1339/**
1340 * Implementation of the function for printing the form elements that control
1341 * whether the course reset functionality affects the quiz.
3b1d5cc4 1342 *
0b5a80a1 1343 * @param $mform form passed by reference
1344 */
1345function quiz_reset_course_form_definition(&$mform) {
c159da4c 1346 $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz'));
0b5a80a1 1347 $mform->addElement('advcheckbox', 'reset_quiz_attempts', get_string('removeallquizattempts','quiz'));
1348}
1349
1350/**
1351 * Course reset form defaults.
8cc86111 1352 * @return array
0b5a80a1 1353 */
1354function quiz_reset_course_form_defaults($course) {
1355 return array('reset_quiz_attempts'=>1);
1356}
1357
1358/**
1359 * Removes all grades from gradebook
8cc86111 1360 *
1361 * @global stdClass
1362 * @global object
0b5a80a1 1363 * @param int $courseid
1364 * @param string optional type
7a6f4066 1365 */
0b5a80a1 1366function quiz_reset_gradebook($courseid, $type='') {
9cf4a18b 1367 global $CFG, $DB;
0b5a80a1 1368
1369 $sql = "SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
9cf4a18b 1370 FROM {quiz} q, {course_modules} cm, {modules} m
1371 WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=q.id AND q.course=?";
0b5a80a1 1372
9cf4a18b 1373 if ($quizs = $DB->get_records_sql($sql, array($courseid))) {
0b5a80a1 1374 foreach ($quizs as $quiz) {
1375 quiz_grade_item_update($quiz, 'reset');
1376 }
1377 }
7a6f4066 1378}
1379
1380/**
72d2982e 1381 * Actual implementation of the reset course functionality, delete all the
7a6f4066 1382 * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is
1383 * set and true.
6ef56c99 1384 *
1385 * Also, move the quiz open and close dates, if the course start date is changing.
8cc86111 1386 *
1387 * @global stdClass
1388 * @global object
1389 * @param object $data the data submitted from the reset course.
0b5a80a1 1390 * @return array status array
7a6f4066 1391 */
0b5a80a1 1392function quiz_reset_userdata($data) {
53004e48 1393 global $CFG, $DB;
1394 require_once($CFG->libdir.'/questionlib.php');
be0ba083 1395
0b5a80a1 1396 $componentstr = get_string('modulenameplural', 'quiz');
1397 $status = array();
b159da78 1398
6ef56c99 1399 /// Delete attempts.
1400 if (!empty($data->reset_quiz_attempts)) {
53004e48 1401 $quizzes = $DB->get_records('quiz', array('course' => $data->courseid));
1402 foreach ($quizzes as $quiz) {
1403 quiz_delete_all_attempts($quiz);
7a6f4066 1404 }
0b5a80a1 1405
0b5a80a1 1406 // remove all grades from gradebook
1407 if (empty($data->reset_gradebook_grades)) {
1408 quiz_reset_gradebook($data->courseid);
7a6f4066 1409 }
53004e48 1410 $status[] = array('component' => $componentstr, 'item' => get_string('attemptsdeleted', 'quiz'), 'error' => false);
7a6f4066 1411 }
6ef56c99 1412
0b5a80a1 1413 /// updating dates - shift may be negative too
1414 if ($data->timeshift) {
1415 shift_course_mod_dates('quiz', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
53004e48 1416 $status[] = array('component' => $componentstr, 'item' => get_string('openclosedatesupdated', 'quiz'), 'error' => false);
7a6f4066 1417 }
0b5a80a1 1418
1419 return $status;
7a6f4066 1420}
14e6dc79 1421
1422/**
1423 * Checks whether the current user is allowed to view a file uploaded in a quiz.
1424 * Teachers can view any from their courses, students can only view their own.
b159da78 1425 *
8cc86111 1426 * @global object
1427 * @global object
1428 * @uses CONTEXT_COURSE
95de57b8 1429 * @param int $attemptuniqueid int attempt id
14e6dc79 1430 * @param int $questionid int question id
b159da78 1431 * @return boolean to indicate access granted or denied
14e6dc79 1432 */
fe6ce234
DC
1433function quiz_check_file_access($attemptuniqueid, $questionid, $context = null) {
1434 global $USER, $DB, $CFG;
1435 require_once(dirname(__FILE__).'/attemptlib.php');
1436 require_once(dirname(__FILE__).'/locallib.php');
b159da78 1437
6102a59d 1438 $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $attemptuniqueid));
fe6ce234
DC
1439 $attemptobj = quiz_attempt::create($attempt->id);
1440
1441 // does question exist?
1442 if (!$question = $DB->get_record('question', array('id' => $questionid))) {
1443 return false;
1444 }
1445
1446 if ($context === null) {
1447 $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz));
1448 $cm = get_coursemodule_from_id('quiz', $quiz->id);
1449 $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1450 }
1451
1452 // Load those questions and the associated states.
1453 $attemptobj->load_questions(array($questionid));
1454 $attemptobj->load_question_states(array($questionid));
1455
1456 // obtain state
1457 $state = $attemptobj->get_question_state($questionid);
1458 // obtain questoin
1459 $question = $attemptobj->get_question($questionid);
b159da78 1460
14e6dc79 1461 // access granted if the current user submitted this file
fe6ce234
DC
1462 if ($attempt->userid != $USER->id) {
1463 return false;
14e6dc79 1464 // access granted if the current user has permission to grade quizzes in this course
fe6ce234
DC
1465 }
1466 if (!(has_capability('mod/quiz:viewreports', $context) || has_capability('mod/quiz:grade', $context))) {
1467 return false;
14e6dc79 1468 }
b159da78 1469
fe6ce234 1470 return array($question, $state, array());
14e6dc79 1471}
b5a16eb7 1472
1473/**
1474 * Prints quiz summaries on MyMoodle Page
8cc86111 1475 *
1476 * @global object
1477 * @global object
1478 * @param arry $courses
1479 * @param array $htmlarray
b5a16eb7 1480 */
1481function quiz_print_overview($courses, &$htmlarray) {
1482 global $USER, $CFG;
1483/// These next 6 Lines are constant in all modules (just change module name)
1484 if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1485 return array();
1486 }
1487
2a13e454 1488 if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
b5a16eb7 1489 return;
1490 }
1491
1492/// Fetch some language strings outside the main loop.
1493 $strquiz = get_string('modulename', 'quiz');
1494 $strnoattempts = get_string('noattempts', 'quiz');
1495
1496/// We want to list quizzes that are currently available, and which have a close date.
1497/// This is the same as what the lesson does, and the dabate is in MDL-10568.
6c58e198 1498 $now = time();
2a13e454 1499 foreach ($quizzes as $quiz) {
b5a16eb7 1500 if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
1501 /// Give a link to the quiz, and the deadline.
1502 $str = '<div class="quiz overview">' .
1503 '<div class="name">' . $strquiz . ': <a ' . ($quiz->visible ? '' : ' class="dimmed"') .
1504 ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->coursemodule . '">' .
1505 $quiz->name . '</a></div>';
1506 $str .= '<div class="info">' . get_string('quizcloseson', 'quiz', userdate($quiz->timeclose)) . '</div>';
1507
1508 /// Now provide more information depending on the uers's role.
1509 $context = get_context_instance(CONTEXT_MODULE, $quiz->coursemodule);
1510 if (has_capability('mod/quiz:viewreports', $context)) {
1511 /// For teacher-like people, show a summary of the number of student attempts.
9cf4a18b 1512 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
2a13e454 1513 // fields set to make the following call work.
7956944f 1514 $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
96c7d771 1515 } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student
b5a16eb7 1516 /// For student-like people, tell them how many attempts they have made.
1517 if (isset($USER->id) && ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
1518 $numattempts = count($attempts);
9cf4a18b 1519 $str .= '<div class="info">' . get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
b5a16eb7 1520 } else {
1521 $str .= '<div class="info">' . $strnoattempts . '</div>';
1522 }
1523 } else {
1524 /// For ayone else, there is no point listing this quiz, so stop processing.
1525 continue;
1526 }
1527
1528 /// Add the output for this quiz to the rest.
1529 $str .= '</div>';
1530 if (empty($htmlarray[$quiz->course]['quiz'])) {
1531 $htmlarray[$quiz->course]['quiz'] = $str;
1532 } else {
1533 $htmlarray[$quiz->course]['quiz'] .= $str;
1534 }
1535 }
1536 }
1537}
6c58e198 1538
1539/**
1540 * Return a textual summary of the number of attemtps that have been made at a particular quiz,
1541 * returns '' if no attemtps have been made yet, unless $returnzero is passed as true.
8cc86111 1542 *
6c58e198 1543 * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
2a13e454 1544 * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
1545 * @param boolean $returnzero if false (default), when no attempts have been made '' is returned instead of 'Attempts: 0'.
1546 * @param int $currentgroup if there is a concept of current group where this method is being called
1547 * (e.g. a report) pass it in here. Default 0 which means no current group.
1548 * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
1549 * "Attemtps 123 (45 from this group)".
6c58e198 1550 */
2a13e454 1551function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
a49cb927 1552 global $DB, $USER;
9cf4a18b 1553 $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0));
6c58e198 1554 if ($numattempts || $returnzero) {
2a13e454 1555 if (groups_get_activity_groupmode($cm)) {
1556 $a->total = $numattempts;
1557 if ($currentgroup) {
9cf4a18b 1558 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1559 '{quiz_attempts} qa JOIN ' .
1560 '{groups_members} gm ON qa.userid = gm.userid ' .
1561 'WHERE quiz = ? AND preview = 0 AND groupid = ?', array($quiz->id, $currentgroup));
2a13e454 1562 return get_string('attemptsnumthisgroup', 'quiz', $a);
9cf4a18b 1563 } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
1564 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups));
1565 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1566 '{quiz_attempts} qa JOIN ' .
1567 '{groups_members} gm ON qa.userid = gm.userid ' .
1568 'WHERE quiz = ? AND preview = 0 AND ' .
1569 "groupid $usql", array_merge(array($quiz->id), $params));
2a13e454 1570 return get_string('attemptsnumyourgroups', 'quiz', $a);
1571 }
1572 }
6c58e198 1573 return get_string('attemptsnum', 'quiz', $numattempts);
1574 }
1575 return '';
1576}
f432bebf 1577
4e781c7b 1578/**
a49cb927
TH
1579 * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
1580 * to the quiz reports.
1581 *
1582 * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1583 * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
1584 * @param object $context the quiz context.
1585 * @param boolean $returnzero if false (default), when no attempts have been made '' is returned instead of 'Attempts: 0'.
1586 * @param int $currentgroup if there is a concept of current group where this method is being called
1587 * (e.g. a report) pass it in here. Default 0 which means no current group.
1588 * @return string HTML fragment for the link.
1589 */
1590function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false, $currentgroup = 0) {
1591 global $CFG;
1592 $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
1593 if (!$summary) {
1594 return '';
1595 }
1596
1597 require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1598 $url = new moodle_url('/mod/quiz/report.php', array(
1599 'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
1600 return html_writer::link($url, $summary);
1601}
1602
1603/**
4e781c7b 1604 * @param string $feature FEATURE_xx constant for requested feature
1605 * @return bool True if quiz supports feature
1606 */
1607function quiz_supports($feature) {
1608 switch($feature) {
42f103be 1609 case FEATURE_GROUPS: return true;
1610 case FEATURE_GROUPINGS: return true;
1611 case FEATURE_GROUPMEMBERSONLY: return true;
dc5c2bd9 1612 case FEATURE_MOD_INTRO: return true;
4e781c7b 1613 case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
42f103be 1614 case FEATURE_GRADE_HAS_GRADE: return true;
1615 case FEATURE_GRADE_OUTCOMES: return true;
767cb7f0 1616 case FEATURE_BACKUP_MOODLE2: return true;
42f103be 1617
49f6e5f4 1618 default: return null;
4e781c7b 1619 }
1620}
1621
f432bebf 1622/**
8cc86111 1623 * @global object
1624 * @global stdClass
cca6e300 1625 * @return array all other caps used in module
f432bebf 1626 */
1627function quiz_get_extra_capabilities() {
d774e817 1628 global $CFG;
be0ba083 1629 require_once($CFG->libdir.'/questionlib.php');
cca6e300 1630 $caps = question_get_all_capabilities();
1631 $caps[] = 'moodle/site:accessallgroups';
1632 return $caps;
f432bebf 1633}
55f599f0 1634
1635/**
792881f0 1636 * This fucntion extends the global navigation for the site.
55f599f0 1637 * It is important to note that you should not rely on PAGE objects within this
1638 * body of code as there is no guarantee that during an AJAX request they are
1639 * available
1640 *
56ed242b 1641 * @param navigation_node $quiznode The quiz node within the global navigation
55f599f0 1642 * @param stdClass $course The course object returned from the DB
1643 * @param stdClass $module The module object returned from the DB
792881f0 1644 * @param stdClass $cm The course module instance returned from the DB
55f599f0 1645 */
56ed242b
SH
1646function quiz_extend_navigation($quiznode, $course, $module, $cm) {
1647 global $CFG;
1648
1649 $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1650
1651 if (has_capability('mod/quiz:view', $context)) {
1652 $url = new moodle_url('/mod/quiz/view.php', array('id'=>$cm->id));
a49cb927
TH
1653 $quiznode->add(get_string('info', 'quiz'), $url, navigation_node::TYPE_SETTING,
1654 null, null, new pix_icon('i/info', ''));
56ed242b
SH
1655 }
1656
1657 if (has_capability('mod/quiz:viewreports', $context)) {
56ed242b
SH
1658 require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php');
1659 $reportlist = quiz_report_list($context);
a49cb927
TH
1660
1661 $url = new moodle_url('/mod/quiz/report.php', array('id' => $cm->id, 'mode' => reset($reportlist)));
1662 $reportnode = $quiznode->add(get_string('results', 'quiz'), $url, navigation_node::TYPE_SETTING,
1663 null, null, new pix_icon('i/report', ''));
1664
56ed242b 1665 foreach ($reportlist as $report) {
a49cb927
TH
1666 $url = new moodle_url('/mod/quiz/report.php', array('id' => $cm->id, 'mode' => $report));
1667 $reportnode->add(get_string($report, 'quiz_'.$report), $url, navigation_node::TYPE_SETTING,
2a8a78c3 1668 null, 'quiz_report_' . $report, new pix_icon('i/item', ''));
56ed242b
SH
1669 }
1670 }
55f599f0 1671}
1672
1673/**
1674 * This function extends the settings navigation block for the site.
1675 *
1676 * It is safe to rely on PAGE here as we will only ever be within the module
1677 * context when this is called
1678 *
0b29477b
SH
1679 * @param settings_navigation $settings
1680 * @param navigation_node $quiznode
55f599f0 1681 */
0b29477b
SH
1682function quiz_extend_settings_navigation($settings, $quiznode) {
1683 global $PAGE, $CFG;
55f599f0 1684
56ed242b
SH
1685 /**
1686 * Require {@link questionlib.php}
1687 * Included here as we only ever want to include this file if we really need to.
1688 */
1689 require_once($CFG->libdir . '/questionlib.php');
55f599f0 1690
56ed242b 1691 if (has_capability('mod/quiz:manageoverrides', $PAGE->cm->context)) {
56ed242b 1692 $url = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$PAGE->cm->id));
eb02301a
TH
1693 $quiznode->add(get_string('groupoverrides', 'quiz'), new moodle_url($url, array('mode'=>'group')),
1694 navigation_node::TYPE_SETTING, null, 'groupoverrides');
1695 $quiznode->add(get_string('useroverrides', 'quiz'), new moodle_url($url, array('mode'=>'user')),
1696 navigation_node::TYPE_SETTING, null, 'useroverrides');
55f599f0 1697 }
56ed242b 1698
55f599f0 1699 if (has_capability('mod/quiz:manage', $PAGE->cm->context)) {
a6855934 1700 $url = new moodle_url('/mod/quiz/edit.php', array('cmid'=>$PAGE->cm->id));
1724eb71 1701 $text = get_string('editquiz', 'quiz');
eb02301a
TH
1702 $quiznode->add($text, $url, navigation_node::TYPE_SETTING, null,
1703 'mod_quiz_edit', new pix_icon('t/edit', ''));
55f599f0 1704 }
56ed242b
SH
1705
1706 if (has_capability('mod/quiz:preview', $PAGE->cm->context)) {
1707 $url = new moodle_url('/mod/quiz/startattempt.php', array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey()));
eb02301a
TH
1708 $quiznode->add(get_string('preview', 'quiz'), $url, navigation_node::TYPE_SETTING,
1709 null, 'mod_quiz_preview', new pix_icon('t/preview', ''));
55f599f0 1710 }
56ed242b 1711
2a8a78c3 1712 question_extend_settings_navigation($quiznode, $PAGE->cm->context)->trim_if_empty();
56ed242b 1713}
fe6ce234
DC
1714
1715/**
1716 * Serves the quiz files.
1717 *
1718 * @param object $course
1719 * @param object $cm
1720 * @param object $context
1721 * @param string $filearea
1722 * @param array $args
1723 * @param bool $forcedownload
1724 * @return bool false if file not found, does not return if found - justsend the file
1725 */
1726function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload) {
1727 global $CFG, $DB;
1728
1729 if ($context->contextlevel != CONTEXT_MODULE) {
1730 return false;
1731 }
1732
1733 require_login($course, false, $cm);
1734
1735 if (!$quiz = $DB->get_record('quiz', array('id'=>$cm->instance))) {
1736 return false;
1737 }
1738
1739 // 'intro' area is served by pluginfile.php
1740 $fileareas = array('feedback');
1741 if (!in_array($filearea, $fileareas)) {
1742 return false;
1743 }
1744
1745 $feedbackid = (int)array_shift($args);
1746 if (!$feedback = $DB->get_record('quiz_feedback', array('id'=>$feedbackid))) {
1747 return false;
1748 }
1749
1750 $fs = get_file_storage();
1751 $relativepath = implode('/', $args);
1752 $fullpath = "/$context->id/mod_quiz/$filearea/$feedbackid/$relativepath";
1753 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1754 return false;
1755 }
1756 send_stored_file($file, 0, 0, true);
1757}
1758
1759/**
1760 * Called via pluginfile.php -> question_pluginfile to serve files belonging to
1761 * a question in a question_attempt when that attempt is a quiz attempt.
1762 *
1763 * @param object $course course settings object
1764 * @param object $context context object
1765 * @param string $component the name of the component we are serving files for.
1766 * @param string $filearea the name of the file area.
1767 * @param array $args the remaining bits of the file path.
1768 * @param bool $forcedownload whether the user must be forced to download the file.
1769 * @return bool false if file not found, does not return if found - justsend the file
1770 */
1771function quiz_question_pluginfile($course, $context, $component,
1772 $filearea, $attemptid, $questionid, $args, $forcedownload) {
1773 global $USER, $CFG;
1774 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
1775
1776 $attemptobj = quiz_attempt::create($attemptid);
1777 require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
1778 $questionids = array($questionid);
1779 $attemptobj->load_questions($questionids);
1780 $attemptobj->load_question_states($questionids);
1781
1782 if ($attemptobj->is_own_attempt() && !$attemptobj->is_finished()) {
1783 // In the middle of an attempt.
1784 if (!$attemptobj->is_preview_user()) {
1785 $attemptobj->require_capability('mod/quiz:attempt');
1786 }
1787 $isreviewing = false;
1788
1789 } else {
1790 // Reviewing an attempt.
1791 $attemptobj->check_review_capability();
1792 $isreviewing = true;
1793 }
1794
1795 if (!$attemptobj->check_file_access($questionid, $isreviewing, $context->id,
1796 $component, $filearea, $args, $forcedownload)) {
1797 send_file_not_found();
1798 }
1799
1800 $fs = get_file_storage();
1801 $relativepath = implode('/', $args);
1802 $fullpath = "/$context->id/$component/$filearea/$relativepath";
1803 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1804 send_file_not_found();
1805 }
1806
1807 send_stored_file($file, 0, 0, $forcedownload);
1808}