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