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