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