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