quiz: MDL-21404 Fix Petr's screw-up when converting to js_writer.
[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);
730fd187 182
53004e48 183 $DB->delete_records('quiz_question_instances', array('quiz' => $quiz->id));
184 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
730fd187 185
53004e48 186 $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id));
187 foreach($events as $event) {
f81a8247
SH
188 $event = calendar_event::load($event);
189 $event->delete();
b2a3cd2d 190 }
191
d6dd2108 192 quiz_grade_item_delete($quiz);
53004e48 193 $DB->delete_records('quiz', array('id' => $quiz->id));
d6dd2108 194
53004e48 195 return true;
196}
197
198/**
199 * Delete all the attempts belonging to a quiz.
8cc86111 200 *
201 * @global stdClass
202 * @global object
203 * @param object $quiz The quiz object.
53004e48 204 */
205function quiz_delete_all_attempts($quiz) {
206 global $CFG, $DB;
207 require_once($CFG->libdir . '/questionlib.php');
208 $attempts = $DB->get_records('quiz_attempts', array('quiz' => $quiz->id));
209 foreach ($attempts as $attempt) {
210 delete_attempt($attempt->uniqueid);
211 }
212 $DB->delete_records('quiz_attempts', array('quiz' => $quiz->id));
213 $DB->delete_records('quiz_grades', array('quiz' => $quiz->id));
730fd187 214}
215
8cc86111 216/**
217 * Return a small object with summary information about what a
218 * user has done with a given particular instance of this module
219 * Used for user activity reports.
220 * $return->time = the time they did it
221 * $return->info = a short text description
222 *
223 * @global object
224 * @param object $course
225 * @param object $user
226 * @param object $mod
227 * @param object $quiz
228 * @return object|null
229 */
730fd187 230function quiz_user_outline($course, $user, $mod, $quiz) {
1a96363a
NC
231 global $DB, $CFG;
232 require_once("$CFG->libdir/gradelib.php");
233 $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
234
235 if (empty($grades->items[0]->grades)) {
236 return null;
237 } else {
238 $grade = reset($grades->items[0]->grades);
5ecfab51 239 }
240
241 $result = new stdClass;
1a96363a
NC
242 $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
243 $result->time = $grade->dategraded;
5ecfab51 244 return $result;
1a96363a 245}
730fd187 246
739b0711 247/**
3b1d5cc4 248 * Is this a graded quiz? If this method returns true, you can assume that
739b0711 249 * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to
250 * divide by them).
251 *
252 * @param object $quiz a row from the quiz table.
253 * @return boolean whether this is a graded quiz.
254 */
255function quiz_has_grades($quiz) {
256 return $quiz->grade != 0 && $quiz->sumgrades != 0;
257}
258
40f6f97f 259/**
260 * Get the best current grade for a particular user in a quiz.
261 *
8cc86111 262 * @global object
40f6f97f 263 * @param object $quiz the quiz object.
264 * @param integer $userid the id of the user.
265 * @return float the user's current grade for this quiz, or NULL if this user does
266 * not have a grade on this quiz.
267 */
268function quiz_get_best_grade($quiz, $userid) {
269 global $DB;
270 $grade = $DB->get_field('quiz_grades', 'grade', array('quiz' => $quiz->id, 'userid' => $userid));
271
272 // Need to detect errors/no result, without catching 0 scores.
273 if (is_numeric($grade)) {
274 return quiz_format_grade($quiz, $grade);
275 } else {
276 return NULL;
277 }
278}
279
8cc86111 280/**
281 * Print a detailed representation of what a user has done with
282 * a given particular instance of this module, for user activity reports.
283 *
284 * @global object
285 * @param object $course
286 * @param object $user
287 * @param object $mod
288 * @param object $quiz
289 * @return bool
290 */
730fd187 291function quiz_user_complete($course, $user, $mod, $quiz) {
1a14a14b 292 global $DB, $CFG, $OUTPUT;
1a96363a
NC
293 require_once("$CFG->libdir/gradelib.php");
294 $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
295 if (!empty($grades->items[0]->grades)) {
296 $grade = reset($grades->items[0]->grades);
297 echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
298 if ($grade->str_feedback) {
299 echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
300 }
301 }
730fd187 302
5ecfab51 303 if ($attempts = $DB->get_records('quiz_attempts', array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) {
ee1fb969 304 foreach ($attempts as $attempt) {
305 echo get_string('attempt', 'quiz').' '.$attempt->attempt.': ';
306 if ($attempt->timefinish == 0) {
307 print_string('unfinished');
308 } else {
84e628a0 309 echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' . quiz_format_grade($quiz, $quiz->sumgrades);
ee1fb969 310 }
311 echo ' - '.userdate($attempt->timemodified).'<br />';
312 }
313 } else {
314 print_string('noattempts', 'quiz');
315 }
316
730fd187 317 return true;
318}
319
8cc86111 320/**
321 * Function to be run periodically according to the moodle cron
322 * This function searches for things that need to be done, such
323 * as sending out mail, toggling flags etc ...
324 *
325 * @global stdClass
326 * @return bool true
327 */
be0ba083 328function quiz_cron() {
730fd187 329 global $CFG;
330
331 return true;
332}
333
b5a16eb7 334/**
8cc86111 335 * @global object
b5a16eb7 336 * @param integer $quizid the quiz id.
337 * @param integer $userid the userid.
338 * @param string $status 'all', 'finished' or 'unfinished' to control
8cc86111 339 * @param bool $includepreviews
b5a16eb7 340 * @return an array of all the user's attempts at this quiz. Returns an empty array if there are none.
341 */
98f38217 342function quiz_get_user_attempts($quizid, $userid=0, $status = 'finished', $includepreviews = false) {
9cf4a18b 343 global $DB;
b5a16eb7 344 $status_condition = array(
345 'all' => '',
346 'finished' => ' AND timefinish > 0',
347 'unfinished' => ' AND timefinish = 0'
348 );
349 $previewclause = '';
350 if (!$includepreviews) {
351 $previewclause = ' AND preview = 0';
352 }
98f38217 353 $params=array($quizid);
354 if ($userid){
355 $userclause = ' AND userid = ?';
356 $params[]=$userid;
357 } else {
358 $userclause = '';
359 }
9cf4a18b 360 if ($attempts = $DB->get_records_select('quiz_attempts',
98f38217 361 "quiz = ?" .$userclause. $previewclause . $status_condition[$status], $params,
b5a16eb7 362 'attempt ASC')) {
363 return $attempts;
364 } else {
365 return array();
366 }
367}
858deff0 368
d6dd2108 369/**
370 * Return grade for given user or all users.
371 *
8cc86111 372 * @global stdClass
373 * @global object
d6dd2108 374 * @param int $quizid id of quiz
375 * @param int $userid optional user id, 0 means all users
f88fb62c 376 * @return array array of grades, false if none. These are raw grades. They should
377 * be processed with quiz_format_grade for display.
d6dd2108 378 */
379function quiz_get_user_grades($quiz, $userid=0) {
9cf4a18b 380 global $CFG, $DB;
d6dd2108 381
9cf4a18b 382 $params = array($quiz->id);
383 $wheresql = '';
384 if ($userid) {
385 $params[] = $userid;
386 $wheresql = "AND u.id = ?";
387 }
d82b018f 388 $sql = "SELECT u.id, u.id AS userid, g.grade AS rawgrade, g.timemodified AS dategraded, MAX(a.timefinish) AS datesubmitted
9cf4a18b 389 FROM {user} u, {quiz_grades} g, {quiz_attempts} a
390 WHERE u.id = g.userid AND g.quiz = ? AND a.quiz = g.quiz AND u.id = a.userid $wheresql
d82b018f 391 GROUP BY u.id, g.grade, g.timemodified";
392
9cf4a18b 393 return $DB->get_records_sql($sql, $params);
d6dd2108 394}
395
f88fb62c 396/**
397 * Round a grade to to the correct number of decimal places, and format it for display.
398 *
399 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
400 * @param float $grade The grade to round.
8cc86111 401 * @return float
f88fb62c 402 */
403function quiz_format_grade($quiz, $grade) {
404 return format_float($grade, $quiz->decimalpoints);
405}
406
84e628a0 407/**
408 * Round a grade to to the correct number of decimal places, and format it for display.
409 *
410 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
411 * @param float $grade The grade to round.
8cc86111 412 * @return float
84e628a0 413 */
414function quiz_format_question_grade($quiz, $grade) {
415 if ($quiz->questiondecimalpoints == -1) {
416 return format_float($grade, $quiz->decimalpoints);
417 } else {
418 return format_float($grade, $quiz->questiondecimalpoints);
419 }
420}
421
d6dd2108 422/**
423 * Update grades in central gradebook
424 *
8cc86111 425 * @global stdClass
426 * @global object
775f811a 427 * @param object $quiz
428 * @param int $userid specific user only, 0 means all
d6dd2108 429 */
775f811a 430function quiz_update_grades($quiz, $userid=0, $nullifnone=true) {
9cf4a18b 431 global $CFG, $DB;
775f811a 432 require_once($CFG->libdir.'/gradelib.php');
ed1daaa9 433
775f811a 434 if ($quiz->grade == 0) {
435 quiz_grade_item_update($quiz);
d6dd2108 436
775f811a 437 } else if ($grades = quiz_get_user_grades($quiz, $userid)) {
438 quiz_grade_item_update($quiz, $grades);
eafb9d9e 439
775f811a 440 } else if ($userid and $nullifnone) {
441 $grade = new object();
442 $grade->userid = $userid;
443 $grade->rawgrade = NULL;
444 quiz_grade_item_update($quiz, $grade);
d6dd2108 445
446 } else {
775f811a 447 quiz_grade_item_update($quiz);
448 }
449}
3b1d5cc4 450
775f811a 451/**
452 * Update all grades in gradebook.
8cc86111 453 *
454 * @global object
775f811a 455 */
456function quiz_upgrade_grades() {
457 global $DB;
458
459 $sql = "SELECT COUNT('x')
460 FROM {quiz} a, {course_modules} cm, {modules} m
461 WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
462 $count = $DB->count_records_sql($sql);
463
464 $sql = "SELECT a.*, cm.idnumber AS cmidnumber, a.course AS courseid
465 FROM {quiz} a, {course_modules} cm, {modules} m
466 WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
467 if ($rs = $DB->get_recordset_sql($sql)) {
775f811a 468 $pbar = new progress_bar('quizupgradegrades', 500, true);
469 $i=0;
470 foreach ($rs as $quiz) {
471 $i++;
472 upgrade_set_timeout(60*5); // set up timeout, may also abort execution
473 quiz_update_grades($quiz, 0, false);
474 $pbar->update($i, $count, "Updating Quiz grades ($i/$count).");
d6dd2108 475 }
775f811a 476 $rs->close();
d6dd2108 477 }
d0ac6bc2 478}
479
d6dd2108 480/**
481 * Create grade item for given quiz
482 *
8cc86111 483 * @global stdClass
484 * @uses GRADE_TYPE_VALUE
485 * @uses GRADE_TYPE_NONE
486 * @uses QUIZ_REVIEW_SCORES
487 * @uses QUIZ_REVIEW_CLOSED
488 * @uses QUIZ_REVIEW_OPEN
489 * @uses PARAM_INT
490 * @uses GRADE_UPDATE_ITEM_LOCKED
d6dd2108 491 * @param object $quiz object with extra cmidnumber
8cc86111 492 * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
d6dd2108 493 * @return int 0 if ok, error code otherwise
494 */
ced5ee59 495function quiz_grade_item_update($quiz, $grades=NULL) {
3b1d5cc4 496 global $CFG, $OUTPUT;
d6dd2108 497 if (!function_exists('grade_update')) { //workaround for buggy PHP versions
498 require_once($CFG->libdir.'/gradelib.php');
499 }
500
501 if (array_key_exists('cmidnumber', $quiz)) { //it may not be always present
502 $params = array('itemname'=>$quiz->name, 'idnumber'=>$quiz->cmidnumber);
503 } else {
504 $params = array('itemname'=>$quiz->name);
505 }
506
507 if ($quiz->grade > 0) {
508 $params['gradetype'] = GRADE_TYPE_VALUE;
509 $params['grademax'] = $quiz->grade;
510 $params['grademin'] = 0;
511
512 } else {
513 $params['gradetype'] = GRADE_TYPE_NONE;
514 }
515
1223d24a 516/* description by TJ:
5171/ If the quiz is set to not show scores while the quiz is still open, and is set to show scores after
518 the quiz is closed, then create the grade_item with a show-after date that is the quiz close date.
5192/ If the quiz is set to not show scores at either of those times, create the grade_item as hidden.
5203/ If the quiz is set to show scores, create the grade_item visible.
521*/
522 if (!($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
523 and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
524 $params['hidden'] = 1;
525
526 } else if ( ($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
527 and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
528 if ($quiz->timeclose) {
529 $params['hidden'] = $quiz->timeclose;
530 } else {
531 $params['hidden'] = 1;
532 }
533
534 } else {
535 // a) both open and closed enabled
536 // b) open enabled, closed disabled - we can not "hide after", grades are kept visible even after closing
537 $params['hidden'] = 0;
538 }
539
0b5a80a1 540 if ($grades === 'reset') {
541 $params['reset'] = true;
542 $grades = NULL;
543 }
9cf4a18b 544
49460d84 545 $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
d45459b7 546 if (!empty($gradebook_grades->items)) {
547 $grade_item = $gradebook_grades->items[0];
548 if ($grade_item->locked) {
549 $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
550 if (!$confirm_regrade) {
551 $message = get_string('gradeitemislocked', 'grades');
552 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id . '&amp;mode=overview';
553 $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
3b1d5cc4 554 echo $OUTPUT->box_start('generalbox', 'notice');
d45459b7 555 echo '<p>'. $message .'</p>';
39e37019 556 echo $OUTPUT->container_start('buttons');
5c2ed7e2
PS
557 echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades'));
558 echo $OUTPUT->single_button($back_link, get_string('cancel'));
39e37019 559 echo $OUTPUT->container_end();
3b1d5cc4 560 echo $OUTPUT->box_end();
9cf4a18b 561
d45459b7 562 return GRADE_UPDATE_ITEM_LOCKED;
563 }
49460d84 564 }
565 }
0b5a80a1 566
ced5ee59 567 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
d6dd2108 568}
569
570/**
571 * Delete grade item for given quiz
572 *
8cc86111 573 * @global stdClass
d6dd2108 574 * @param object $quiz object
575 * @return object quiz
576 */
577function quiz_grade_item_delete($quiz) {
578 global $CFG;
53004e48 579 require_once($CFG->libdir . '/gradelib.php');
d6dd2108 580
53004e48 581 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, NULL, array('deleted' => 1));
d6dd2108 582}
583
e2249afe 584/**
585 * @return the options for calculating the quiz grade from the individual attempt grades.
586 */
587function quiz_get_grading_options() {
588 return array (
589 QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'),
590 QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'),
591 QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'),
592 QUIZ_ATTEMPTLAST => get_string('attemptlast', 'quiz'));
593}
d6dd2108 594
8cc86111 595/**
596 * Returns an array of users who have data in a given quiz
597 *
598 * @global stdClass
599 * @global object
600 * @param int $quizid
601 * @return array
602 */
d061d883 603function quiz_get_participants($quizid) {
9cf4a18b 604 global $CFG, $DB;
d061d883 605
e4acc4ce 606 //Get users from attempts
9cf4a18b 607 $us_attempts = $DB->get_records_sql("SELECT DISTINCT u.id, u.id
608 FROM {user} u,
609 {quiz_attempts} a
610 WHERE a.quiz = ? and
611 u.id = a.userid", array($quizid));
e4acc4ce 612
e4acc4ce 613 //Return us_attempts array (it contains an array of unique users)
6b224376 614 return $us_attempts;
e4acc4ce 615
d061d883 616}
730fd187 617
8cc86111 618/**
619 * This standard function will check all instances of this module
620 * and make sure there are up-to-date events created for each of them.
621 * If courseid = 0, then every quiz event in the site is checked, else
622 * only quiz events belonging to the course specified are checked.
623 * This function is used, in its new format, by restore_refresh_events()
624 *
625 * @global object
626 * @uses QUIZ_MAX_EVENT_LENGTH
627 * @param int $courseid
628 * @return bool
629 */
d2f308c0 630function quiz_refresh_events($courseid = 0) {
9cf4a18b 631 global $DB;
d2f308c0 632
633 if ($courseid == 0) {
9cf4a18b 634 if (! $quizzes = $DB->get_records('quiz')) {
d2f308c0 635 return true;
636 }
637 } else {
9cf4a18b 638 if (! $quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
d2f308c0 639 return true;
640 }
641 }
9cf4a18b 642 $moduleid = $DB->get_field('modules', 'id', array('name' => 'quiz'));
f41e824f 643
d2f308c0 644 foreach ($quizzes as $quiz) {
0cd1affc 645 $cm = get_coursemodule_from_id('quiz', $quiz->id);
d2f308c0 646 $event = NULL;
b2a3cd2d 647 $event2 = NULL;
648 $event2old = NULL;
649
53004e48 650 if ($events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id), 'timestart')) {
b2a3cd2d 651 $event = array_shift($events);
652 if (!empty($events)) {
653 $event2old = array_shift($events);
654 if (!empty($events)) {
655 foreach ($events as $badevent) {
f81a8247
SH
656 $badevent = calendar_event::load($badevent);
657 $badevent->delete();
b2a3cd2d 658 }
659 }
660 }
661 }
662
ae040d4b 663 $event->name = $quiz->name;
0cd1affc 664 $event->description = format_module_intro('quiz', $quiz, $cm->id);
b2a3cd2d 665 $event->courseid = $quiz->course;
666 $event->groupid = 0;
667 $event->userid = 0;
668 $event->modulename = 'quiz';
669 $event->instance = $quiz->id;
ba288539 670 $event->visible = instance_is_visible('quiz', $quiz);
d2f308c0 671 $event->timestart = $quiz->timeopen;
b2a3cd2d 672 $event->eventtype = 'open';
d2f308c0 673 $event->timeduration = ($quiz->timeclose - $quiz->timeopen);
d2f308c0 674
b2a3cd2d 675 if ($event->timeduration > QUIZ_MAX_EVENT_LENGTH) { /// Set up two events
d2f308c0 676
b2a3cd2d 677 $event2 = $event;
d2f308c0 678
ae040d4b 679 $event->name = $quiz->name.' ('.get_string('quizopens', 'quiz').')';
b2a3cd2d 680 $event->timeduration = 0;
681
ae040d4b 682 $event2->name = $quiz->name.' ('.get_string('quizcloses', 'quiz').')';
b2a3cd2d 683 $event2->timestart = $quiz->timeclose;
684 $event2->eventtype = 'close';
685 $event2->timeduration = 0;
686
687 if (empty($event2old->id)) {
688 unset($event2->id);
f81a8247 689 calendar_event::create($event2);
b2a3cd2d 690 } else {
691 $event2->id = $event2old->id;
f81a8247
SH
692 $event2 = calendar_event::load($event2);
693 $event2->update($event2);
b2a3cd2d 694 }
acda04bf 695 } else if (!empty($event2old->id)) {
f81a8247
SH
696 $event2old = calendar_event::load($event2old);
697 $event2old->delete();
b2a3cd2d 698 }
699
700 if (empty($event->id)) {
acda04bf 701 if (!empty($event->timestart)) {
f81a8247 702 calendar_event::create($event);
acda04bf 703 }
b2a3cd2d 704 } else {
f81a8247
SH
705 $event = calendar_event::load($event);
706 $event->update($event);
d2f308c0 707 }
b2a3cd2d 708
d2f308c0 709 }
710 return true;
711}
712
dd97c328 713/**
714 * Returns all quiz graded users since a given time for specified quiz
8cc86111 715 *
716 * @global stdClass
717 * @global object
718 * @global object
719 * @global object
720 * @uses CONTEXT_MODULE
721 * @param array $activities By reference
722 * @param int $index By reference
723 * @param int $timestart
724 * @param int $courseid
725 * @param int $cmid
726 * @param int $userid
727 * @param int $groupid
728 * @return void
dd97c328 729 */
730function quiz_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid=0, $groupid=0) {
9cf4a18b 731 global $CFG, $COURSE, $USER, $DB;
6710ec87 732
dd97c328 733 if ($COURSE->id == $courseid) {
734 $course = $COURSE;
6710ec87 735 } else {
9cf4a18b 736 $course = $DB->get_record('course', array('id' => $courseid));
6710ec87 737 }
6710ec87 738
dd97c328 739 $modinfo =& get_fast_modinfo($course);
6710ec87 740
dd97c328 741 $cm = $modinfo->cms[$cmid];
6710ec87 742
9cf4a18b 743 $params = array($timestart, $cm->instance);
744
dd97c328 745 if ($userid) {
9cf4a18b 746 $userselect = "AND u.id = ?";
747 $params[] = $userid;
dd97c328 748 } else {
749 $userselect = "";
750 }
ac21ad39 751
dd97c328 752 if ($groupid) {
9cf4a18b 753 $groupselect = "AND gm.groupid = ?";
754 $groupjoin = "JOIN {groups_members} gm ON gm.userid=u.id";
755 $params[] = $groupid;
dd97c328 756 } else {
757 $groupselect = "";
758 $groupjoin = "";
759 }
6710ec87 760
9cf4a18b 761 if (!$attempts = $DB->get_records_sql("SELECT qa.*, q.sumgrades AS maxgrade,
762 u.firstname, u.lastname, u.email, u.picture
763 FROM {quiz_attempts} qa
764 JOIN {quiz} q ON q.id = qa.quiz
765 JOIN {user} u ON u.id = qa.userid
dd97c328 766 $groupjoin
767 WHERE qa.timefinish > $timestart AND q.id = $cm->instance
768 $userselect $groupselect
9cf4a18b 769 ORDER BY qa.timefinish ASC", $params)) {
dd97c328 770 return;
771 }
6710ec87 772
dd97c328 773 $cm_context = get_context_instance(CONTEXT_MODULE, $cm->id);
774 $grader = has_capability('moodle/grade:viewall', $cm_context);
775 $accessallgroups = has_capability('moodle/site:accessallgroups', $cm_context);
776 $viewfullnames = has_capability('moodle/site:viewfullnames', $cm_context);
777 $grader = has_capability('mod/quiz:grade', $cm_context);
778 $groupmode = groups_get_activity_groupmode($cm, $course);
6710ec87 779
dd97c328 780 if (is_null($modinfo->groups)) {
781 $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
782 }
6710ec87 783
dd97c328 784 $aname = format_string($cm->name,true);
785 foreach ($attempts as $attempt) {
786 if ($attempt->userid != $USER->id) {
787 if (!$grader) {
788 // grade permission required
789 continue;
790 }
6710ec87 791
9cf4a18b 792 if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
dd97c328 793 $usersgroups = groups_get_all_groups($course->id, $attempt->userid, $cm->groupingid);
794 if (!is_array($usersgroups)) {
795 continue;
796 }
797 $usersgroups = array_keys($usersgroups);
798 $interset = array_intersect($usersgroups, $modinfo->groups[$cm->id]);
799 if (empty($intersect)) {
800 continue;
801 }
802 }
803 }
804
805 $tmpactivity = new object();
806
807 $tmpactivity->type = 'quiz';
808 $tmpactivity->cmid = $cm->id;
809 $tmpactivity->name = $aname;
76cbde41 810 $tmpactivity->sectionnum= $cm->sectionnum;
dd97c328 811 $tmpactivity->timestamp = $attempt->timefinish;
9cf4a18b 812
dd97c328 813 $tmpactivity->content->attemptid = $attempt->id;
814 $tmpactivity->content->sumgrades = $attempt->sumgrades;
815 $tmpactivity->content->maxgrade = $attempt->maxgrade;
816 $tmpactivity->content->attempt = $attempt->attempt;
9cf4a18b 817
dd97c328 818 $tmpactivity->user->userid = $attempt->userid;
819 $tmpactivity->user->fullname = fullname($attempt, $viewfullnames);
820 $tmpactivity->user->picture = $attempt->picture;
9cf4a18b 821
dd97c328 822 $activities[$index++] = $tmpactivity;
6710ec87 823 }
824
825 return;
826}
827
8cc86111 828/**
829 * @global stdClass
830 * @param object $activity
831 * @param int $courseid
832 * @param bool $detail
833 * @param array $modnames
834 * @return void output is echo'd
835 */
dd97c328 836function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
e63f88c9 837 global $CFG, $OUTPUT;
6710ec87 838
dd97c328 839 echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">';
6710ec87 840
dd97c328 841 echo "<tr><td class=\"userpicture\" valign=\"top\">";
812dbaf7 842 echo $OUTPUT->user_picture($activity->user, array('courseid'=>$courseid));
dd97c328 843 echo "</td><td>";
6710ec87 844
845 if ($detail) {
dd97c328 846 $modname = $modnames[$activity->type];
847 echo '<div class="title">';
b5d0cafc 848 echo "<img src=\"" . $OUTPUT->pix_url('icon', $activity->type) . "\" ".
dd97c328 849 "class=\"icon\" alt=\"$modname\" />";
850 echo "<a href=\"$CFG->wwwroot/mod/quiz/view.php?id={$activity->cmid}\">{$activity->name}</a>";
851 echo '</div>';
6710ec87 852 }
853
dd97c328 854 echo '<div class="grade">';
855 echo get_string("attempt", "quiz")." {$activity->content->attempt}: ";
856 $grades = "({$activity->content->sumgrades} / {$activity->content->maxgrade})";
857 echo "<a href=\"$CFG->wwwroot/mod/quiz/review.php?attempt={$activity->content->attemptid}\">$grades</a>";
858 echo '</div>';
6710ec87 859
dd97c328 860 echo '<div class="user">';
861 echo "<a href=\"$CFG->wwwroot/user/view.php?id={$activity->user->userid}&amp;course=$courseid\">"
862 ."{$activity->user->fullname}</a> - ".userdate($activity->timestamp);
863 echo '</div>';
6710ec87 864
dd97c328 865 echo "</td></tr></table>";
6710ec87 866
867 return;
868}
869
ee1fb969 870/**
920b93d1 871 * Pre-process the quiz options form data, making any necessary adjustments.
ad4cd837 872 * Called by add/update instance in this file.
b159da78 873 *
8cc86111 874 * @uses QUIZ_REVIEW_OVERALLFEEDBACK
875 * @uses QUIZ_REVIEW_CLOSED
876 * @uses QUIZ_REVIEW_OPEN
877 * @uses QUIZ_REVIEW_IMMEDIATELY
878 * @uses QUIZ_REVIEW_GENERALFEEDBACK
879 * @uses QUIZ_REVIEW_SOLUTIONS
880 * @uses QUIZ_REVIEW_ANSWERS
881 * @uses QUIZ_REVIEW_FEEDBACK
882 * @uses QUIZ_REVIEW_SCORES
883 * @uses QUIZ_REVIEW_RESPONSES
884 * @uses QUESTION_ADAPTIVE
920b93d1 885 * @param object $quiz The variables set on the form.
8cc86111 886 * @return string
920b93d1 887 */
888function quiz_process_options(&$quiz) {
889 $quiz->timemodified = time();
ee1fb969 890
dc5c6851 891 // Quiz name.
892 if (!empty($quiz->name)) {
893 $quiz->name = trim($quiz->name);
894 }
a23f0aaf 895
ab0a8ff2 896 // Password field - different in form to stop browsers that remember passwords
897 // getting confused.
898 $quiz->password = $quiz->quizpassword;
899 unset($quiz->quizpassword);
900
212b7b8c 901 // Quiz feedback
a0807a00 902 if (isset($quiz->feedbacktext)) {
903 // Clean up the boundary text.
904 for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
905 if (empty($quiz->feedbacktext[$i])) {
906 $quiz->feedbacktext[$i] = '';
907 } else {
908 $quiz->feedbacktext[$i] = trim($quiz->feedbacktext[$i]);
909 }
212b7b8c 910 }
b159da78 911
a0807a00 912 // Check the boundary value is a number or a percentage, and in range.
913 $i = 0;
914 while (!empty($quiz->feedbackboundaries[$i])) {
915 $boundary = trim($quiz->feedbackboundaries[$i]);
916 if (!is_numeric($boundary)) {
917 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
918 $boundary = trim(substr($boundary, 0, -1));
919 if (is_numeric($boundary)) {
920 $boundary = $boundary * $quiz->grade / 100.0;
921 } else {
922 return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
923 }
212b7b8c 924 }
925 }
a0807a00 926 if ($boundary <= 0 || $boundary >= $quiz->grade) {
927 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
928 }
929 if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
930 return get_string('feedbackerrororder', 'quiz', $i + 1);
931 }
932 $quiz->feedbackboundaries[$i] = $boundary;
933 $i += 1;
212b7b8c 934 }
a0807a00 935 $numboundaries = $i;
b159da78 936
a0807a00 937 // Check there is nothing in the remaining unused fields.
e0b7cfcb 938 if (!empty($quiz->feedbackboundaries)) {
939 for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
940 if (!empty($quiz->feedbackboundaries[$i]) && trim($quiz->feedbackboundaries[$i]) != '') {
941 return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
942 }
a0807a00 943 }
212b7b8c 944 }
a0807a00 945 for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
946 if (!empty($quiz->feedbacktext[$i]) && trim($quiz->feedbacktext[$i]) != '') {
947 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
948 }
212b7b8c 949 }
a0807a00 950 $quiz->feedbackboundaries[-1] = $quiz->grade + 1; // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
951 $quiz->feedbackboundaries[$numboundaries] = 0;
952 $quiz->feedbackboundarycount = $numboundaries;
212b7b8c 953 }
a23f0aaf 954
920b93d1 955 // Settings that get combined to go into the optionflags column.
956 $quiz->optionflags = 0;
957 if (!empty($quiz->adaptive)) {
958 $quiz->optionflags |= QUESTION_ADAPTIVE;
959 }
960
961 // Settings that get combined to go into the review column.
962 $review = 0;
963 if (isset($quiz->responsesimmediately)) {
ee1fb969 964 $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 965 unset($quiz->responsesimmediately);
ee1fb969 966 }
920b93d1 967 if (isset($quiz->responsesopen)) {
ee1fb969 968 $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN);
920b93d1 969 unset($quiz->responsesopen);
ee1fb969 970 }
920b93d1 971 if (isset($quiz->responsesclosed)) {
ee1fb969 972 $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED);
920b93d1 973 unset($quiz->responsesclosed);
ee1fb969 974 }
975
920b93d1 976 if (isset($quiz->scoreimmediately)) {
ee1fb969 977 $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 978 unset($quiz->scoreimmediately);
ee1fb969 979 }
920b93d1 980 if (isset($quiz->scoreopen)) {
ee1fb969 981 $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN);
920b93d1 982 unset($quiz->scoreopen);
ee1fb969 983 }
920b93d1 984 if (isset($quiz->scoreclosed)) {
ee1fb969 985 $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED);
920b93d1 986 unset($quiz->scoreclosed);
ee1fb969 987 }
988
920b93d1 989 if (isset($quiz->feedbackimmediately)) {
ee1fb969 990 $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 991 unset($quiz->feedbackimmediately);
ee1fb969 992 }
920b93d1 993 if (isset($quiz->feedbackopen)) {
ee1fb969 994 $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN);
920b93d1 995 unset($quiz->feedbackopen);
ee1fb969 996 }
920b93d1 997 if (isset($quiz->feedbackclosed)) {
ee1fb969 998 $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED);
920b93d1 999 unset($quiz->feedbackclosed);
ee1fb969 1000 }
1001
920b93d1 1002 if (isset($quiz->answersimmediately)) {
ee1fb969 1003 $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 1004 unset($quiz->answersimmediately);
ee1fb969 1005 }
920b93d1 1006 if (isset($quiz->answersopen)) {
ee1fb969 1007 $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN);
920b93d1 1008 unset($quiz->answersopen);
ee1fb969 1009 }
920b93d1 1010 if (isset($quiz->answersclosed)) {
ee1fb969 1011 $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED);
920b93d1 1012 unset($quiz->answersclosed);
ee1fb969 1013 }
1014
920b93d1 1015 if (isset($quiz->solutionsimmediately)) {
ee1fb969 1016 $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 1017 unset($quiz->solutionsimmediately);
ee1fb969 1018 }
920b93d1 1019 if (isset($quiz->solutionsopen)) {
ee1fb969 1020 $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_OPEN);
920b93d1 1021 unset($quiz->solutionsopen);
ee1fb969 1022 }
920b93d1 1023 if (isset($quiz->solutionsclosed)) {
ee1fb969 1024 $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_CLOSED);
920b93d1 1025 unset($quiz->solutionsclosed);
ee1fb969 1026 }
1027
a4514d91 1028 if (isset($quiz->generalfeedbackimmediately)) {
1029 $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
00719c02 1030 unset($quiz->generalfeedbackimmediately);
1b8a7434 1031 }
a4514d91 1032 if (isset($quiz->generalfeedbackopen)) {
1033 $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_OPEN);
00719c02 1034 unset($quiz->generalfeedbackopen);
1b8a7434 1035 }
a4514d91 1036 if (isset($quiz->generalfeedbackclosed)) {
1037 $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_CLOSED);
00719c02 1038 unset($quiz->generalfeedbackclosed);
1039 }
1040
1041 if (isset($quiz->overallfeedbackimmediately)) {
1042 $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1043 unset($quiz->overallfeedbackimmediately);
1044 }
1045 if (isset($quiz->overallfeedbackopen)) {
1046 $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_OPEN);
1047 unset($quiz->overallfeedbackopen);
1048 }
1049 if (isset($quiz->overallfeedbackclosed)) {
1050 $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_CLOSED);
1051 unset($quiz->overallfeedbackclosed);
1b8a7434 1052 }
1053
920b93d1 1054 $quiz->review = $review;
1055}
1056
1057/**
1058 * This function is called at the end of quiz_add_instance
1059 * and quiz_update_instance, to do the common processing.
a23f0aaf 1060 *
8cc86111 1061 * @global object
1062 * @uses QUIZ_MAX_EVENT_LENGTH
920b93d1 1063 * @param object $quiz the quiz object.
8cc86111 1064 * @return void|string Void or error message
920b93d1 1065 */
1066function quiz_after_add_or_update($quiz) {
c18269c7 1067 global $DB;
920b93d1 1068
212b7b8c 1069 // Save the feedback
53004e48 1070 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
a23f0aaf 1071
212b7b8c 1072 for ($i = 0; $i <= $quiz->feedbackboundarycount; $i += 1) {
1073 $feedback = new stdClass;
1074 $feedback->quizid = $quiz->id;
1075 $feedback->feedbacktext = $quiz->feedbacktext[$i];
1076 $feedback->mingrade = $quiz->feedbackboundaries[$i];
1077 $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
fc29e51b 1078 $DB->insert_record('quiz_feedback', $feedback, false);
212b7b8c 1079 }
1080
920b93d1 1081 // Update the events relating to this quiz.
1082 // This is slightly inefficient, deleting the old events and creating new ones. However,
1083 // there are at most two events, and this keeps the code simpler.
c18269c7 1084 if ($events = $DB->get_records('event', array('modulename'=>'quiz', 'instance'=>$quiz->id))) {
920b93d1 1085 foreach($events as $event) {
f81a8247
SH
1086 $event2old = calendar_event::load($event);
1087 $event2old->delete();
920b93d1 1088 }
1089 }
1090
1091 $event = new stdClass;
1092 $event->description = $quiz->intro;
1093 $event->courseid = $quiz->course;
1094 $event->groupid = 0;
1095 $event->userid = 0;
1096 $event->modulename = 'quiz';
1097 $event->instance = $quiz->id;
1098 $event->timestart = $quiz->timeopen;
1099 $event->timeduration = $quiz->timeclose - $quiz->timeopen;
1100 $event->visible = instance_is_visible('quiz', $quiz);
1101 $event->eventtype = 'open';
1102
1103 if ($quiz->timeclose and $quiz->timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) {
1104 // Single event for the whole quiz.
1105 $event->name = $quiz->name;
f81a8247 1106 calendar_event::create($event);
920b93d1 1107 } else {
1108 // Separate start and end events.
1109 $event->timeduration = 0;
1110 if ($quiz->timeopen) {
1111 $event->name = $quiz->name.' ('.get_string('quizopens', 'quiz').')';
f81a8247 1112 calendar_event::create($event);
920b93d1 1113 unset($event->id); // So we can use the same object for the close event.
1114 }
1115 if ($quiz->timeclose) {
1116 $event->name = $quiz->name.' ('.get_string('quizcloses', 'quiz').')';
1117 $event->timestart = $quiz->timeclose;
1118 $event->eventtype = 'close';
f81a8247 1119 calendar_event::create($event);
920b93d1 1120 }
1121 }
d6dd2108 1122
1123 //update related grade item
c18269c7 1124 quiz_grade_item_update($quiz);
ee1fb969 1125}
1126
8cc86111 1127/**
1128 * @return array
1129 */
f3221af9 1130function quiz_get_view_actions() {
acf149ad 1131 return array('view', 'view all', 'report', 'review');
f3221af9 1132}
ee1fb969 1133
8cc86111 1134/**
1135 * @return array
1136 */
f3221af9 1137function quiz_get_post_actions() {
acf149ad 1138 return array('attempt', 'close attempt', 'preview', 'editquestions', 'delete attempt', 'manualgrade');
f3221af9 1139}
ee1fb969 1140
f67172b6 1141/**
1142 * Returns an array of names of quizzes that use this question
1143 *
64d79492 1144 * @param integer $questionid
f67172b6 1145 * @return array of strings
1146 */
1147function quiz_question_list_instances($questionid) {
c6307ef2 1148 global $CFG, $DB;
e8666d9a 1149
64d79492 1150 // TODO MDL-5780: we should also consider other questions that are used by
e8666d9a 1151 // random questions in this quiz, but that is very hard.
1152
1153 $sql = "SELECT q.id, q.name
c6307ef2 1154 FROM {quiz} q
1155 JOIN {quiz_question_instances} qqi ON q.id = qqi.quiz
1156 WHERE qqi.question = ?";
e8666d9a 1157
c6307ef2 1158 if ($instances = $DB->get_records_sql_menu($sql, array($questionid))) {
e8666d9a 1159 return $instances;
1160 }
f67172b6 1161 return array();
1162}
1163
7a6f4066 1164/**
1165 * Implementation of the function for printing the form elements that control
1166 * whether the course reset functionality affects the quiz.
3b1d5cc4 1167 *
0b5a80a1 1168 * @param $mform form passed by reference
1169 */
1170function quiz_reset_course_form_definition(&$mform) {
c159da4c 1171 $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz'));
0b5a80a1 1172 $mform->addElement('advcheckbox', 'reset_quiz_attempts', get_string('removeallquizattempts','quiz'));
1173}
1174
1175/**
1176 * Course reset form defaults.
8cc86111 1177 * @return array
0b5a80a1 1178 */
1179function quiz_reset_course_form_defaults($course) {
1180 return array('reset_quiz_attempts'=>1);
1181}
1182
1183/**
1184 * Removes all grades from gradebook
8cc86111 1185 *
1186 * @global stdClass
1187 * @global object
0b5a80a1 1188 * @param int $courseid
1189 * @param string optional type
7a6f4066 1190 */
0b5a80a1 1191function quiz_reset_gradebook($courseid, $type='') {
9cf4a18b 1192 global $CFG, $DB;
0b5a80a1 1193
1194 $sql = "SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
9cf4a18b 1195 FROM {quiz} q, {course_modules} cm, {modules} m
1196 WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=q.id AND q.course=?";
0b5a80a1 1197
9cf4a18b 1198 if ($quizs = $DB->get_records_sql($sql, array($courseid))) {
0b5a80a1 1199 foreach ($quizs as $quiz) {
1200 quiz_grade_item_update($quiz, 'reset');
1201 }
1202 }
7a6f4066 1203}
1204
1205/**
1206 * Actual implementation of the rest coures functionality, delete all the
1207 * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is
1208 * set and true.
6ef56c99 1209 *
1210 * Also, move the quiz open and close dates, if the course start date is changing.
8cc86111 1211 *
1212 * @global stdClass
1213 * @global object
1214 * @param object $data the data submitted from the reset course.
0b5a80a1 1215 * @return array status array
7a6f4066 1216 */
0b5a80a1 1217function quiz_reset_userdata($data) {
53004e48 1218 global $CFG, $DB;
1219 require_once($CFG->libdir.'/questionlib.php');
be0ba083 1220
0b5a80a1 1221 $componentstr = get_string('modulenameplural', 'quiz');
1222 $status = array();
b159da78 1223
6ef56c99 1224 /// Delete attempts.
1225 if (!empty($data->reset_quiz_attempts)) {
53004e48 1226 $quizzes = $DB->get_records('quiz', array('course' => $data->courseid));
1227 foreach ($quizzes as $quiz) {
1228 quiz_delete_all_attempts($quiz);
7a6f4066 1229 }
0b5a80a1 1230
0b5a80a1 1231 // remove all grades from gradebook
1232 if (empty($data->reset_gradebook_grades)) {
1233 quiz_reset_gradebook($data->courseid);
7a6f4066 1234 }
53004e48 1235 $status[] = array('component' => $componentstr, 'item' => get_string('attemptsdeleted', 'quiz'), 'error' => false);
7a6f4066 1236 }
6ef56c99 1237
0b5a80a1 1238 /// updating dates - shift may be negative too
1239 if ($data->timeshift) {
1240 shift_course_mod_dates('quiz', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
53004e48 1241 $status[] = array('component' => $componentstr, 'item' => get_string('openclosedatesupdated', 'quiz'), 'error' => false);
7a6f4066 1242 }
0b5a80a1 1243
1244 return $status;
7a6f4066 1245}
14e6dc79 1246
1247/**
1248 * Checks whether the current user is allowed to view a file uploaded in a quiz.
1249 * Teachers can view any from their courses, students can only view their own.
b159da78 1250 *
8cc86111 1251 * @global object
1252 * @global object
1253 * @uses CONTEXT_COURSE
95de57b8 1254 * @param int $attemptuniqueid int attempt id
14e6dc79 1255 * @param int $questionid int question id
b159da78 1256 * @return boolean to indicate access granted or denied
14e6dc79 1257 */
95de57b8 1258function quiz_check_file_access($attemptuniqueid, $questionid) {
9cf4a18b 1259 global $USER, $DB;
b159da78 1260
6102a59d 1261 $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $attemptuniqueid));
9cf4a18b 1262 $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz));
14e6dc79 1263 $context = get_context_instance(CONTEXT_COURSE, $quiz->course);
b159da78 1264
14e6dc79 1265 // access granted if the current user submitted this file
1266 if ($attempt->userid == $USER->id) {
1267 return true;
1268 // access granted if the current user has permission to grade quizzes in this course
1269 } else if (has_capability('mod/quiz:viewreports', $context) || has_capability('mod/quiz:grade', $context)) {
1270 return true;
1271 }
b159da78 1272
1273 // otherwise, this user does not have permission
14e6dc79 1274 return false;
1275}
b5a16eb7 1276
1277/**
1278 * Prints quiz summaries on MyMoodle Page
8cc86111 1279 *
1280 * @global object
1281 * @global object
1282 * @param arry $courses
1283 * @param array $htmlarray
b5a16eb7 1284 */
1285function quiz_print_overview($courses, &$htmlarray) {
1286 global $USER, $CFG;
1287/// These next 6 Lines are constant in all modules (just change module name)
1288 if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1289 return array();
1290 }
1291
2a13e454 1292 if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
b5a16eb7 1293 return;
1294 }
1295
1296/// Fetch some language strings outside the main loop.
1297 $strquiz = get_string('modulename', 'quiz');
1298 $strnoattempts = get_string('noattempts', 'quiz');
1299
1300/// We want to list quizzes that are currently available, and which have a close date.
1301/// This is the same as what the lesson does, and the dabate is in MDL-10568.
6c58e198 1302 $now = time();
2a13e454 1303 foreach ($quizzes as $quiz) {
b5a16eb7 1304 if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
1305 /// Give a link to the quiz, and the deadline.
1306 $str = '<div class="quiz overview">' .
1307 '<div class="name">' . $strquiz . ': <a ' . ($quiz->visible ? '' : ' class="dimmed"') .
1308 ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->coursemodule . '">' .
1309 $quiz->name . '</a></div>';
1310 $str .= '<div class="info">' . get_string('quizcloseson', 'quiz', userdate($quiz->timeclose)) . '</div>';
1311
1312 /// Now provide more information depending on the uers's role.
1313 $context = get_context_instance(CONTEXT_MODULE, $quiz->coursemodule);
1314 if (has_capability('mod/quiz:viewreports', $context)) {
1315 /// For teacher-like people, show a summary of the number of student attempts.
9cf4a18b 1316 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
2a13e454 1317 // fields set to make the following call work.
7956944f 1318 $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
96c7d771 1319 } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student
b5a16eb7 1320 /// For student-like people, tell them how many attempts they have made.
1321 if (isset($USER->id) && ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
1322 $numattempts = count($attempts);
9cf4a18b 1323 $str .= '<div class="info">' . get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
b5a16eb7 1324 } else {
1325 $str .= '<div class="info">' . $strnoattempts . '</div>';
1326 }
1327 } else {
1328 /// For ayone else, there is no point listing this quiz, so stop processing.
1329 continue;
1330 }
1331
1332 /// Add the output for this quiz to the rest.
1333 $str .= '</div>';
1334 if (empty($htmlarray[$quiz->course]['quiz'])) {
1335 $htmlarray[$quiz->course]['quiz'] = $str;
1336 } else {
1337 $htmlarray[$quiz->course]['quiz'] .= $str;
1338 }
1339 }
1340 }
1341}
6c58e198 1342
1343/**
1344 * Return a textual summary of the number of attemtps that have been made at a particular quiz,
1345 * returns '' if no attemtps have been made yet, unless $returnzero is passed as true.
8cc86111 1346 *
1347 * @global stdClass
1348 * @global object
1349 * @global object
6c58e198 1350 * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
2a13e454 1351 * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
1352 * @param boolean $returnzero if false (default), when no attempts have been made '' is returned instead of 'Attempts: 0'.
1353 * @param int $currentgroup if there is a concept of current group where this method is being called
1354 * (e.g. a report) pass it in here. Default 0 which means no current group.
1355 * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
1356 * "Attemtps 123 (45 from this group)".
6c58e198 1357 */
2a13e454 1358function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
9cf4a18b 1359 global $CFG, $USER, $DB;
1360 $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0));
6c58e198 1361 if ($numattempts || $returnzero) {
2a13e454 1362 if (groups_get_activity_groupmode($cm)) {
1363 $a->total = $numattempts;
1364 if ($currentgroup) {
9cf4a18b 1365 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1366 '{quiz_attempts} qa JOIN ' .
1367 '{groups_members} gm ON qa.userid = gm.userid ' .
1368 'WHERE quiz = ? AND preview = 0 AND groupid = ?', array($quiz->id, $currentgroup));
2a13e454 1369 return get_string('attemptsnumthisgroup', 'quiz', $a);
9cf4a18b 1370 } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
1371 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups));
1372 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1373 '{quiz_attempts} qa JOIN ' .
1374 '{groups_members} gm ON qa.userid = gm.userid ' .
1375 'WHERE quiz = ? AND preview = 0 AND ' .
1376 "groupid $usql", array_merge(array($quiz->id), $params));
2a13e454 1377 return get_string('attemptsnumyourgroups', 'quiz', $a);
1378 }
1379 }
6c58e198 1380 return get_string('attemptsnum', 'quiz', $numattempts);
1381 }
1382 return '';
1383}
f432bebf 1384
4e781c7b 1385/**
8cc86111 1386 * @uses FEATURE_GROUPS
1387 * @uses FEATURE_GROUPINGS
1388 * @uses FEATURE_GROUPMEMBERSONLY
1389 * @uses FEATURE_MOD_INTRO
1390 * @uses FEATURE_COMPLETION_TRACKS_VIEWS
1391 * @uses FEATURE_GRADE_HAS_GRADE
1392 * @uses FEATURE_GRADE_OUTCOMES
4e781c7b 1393 * @param string $feature FEATURE_xx constant for requested feature
1394 * @return bool True if quiz supports feature
1395 */
1396function quiz_supports($feature) {
1397 switch($feature) {
42f103be 1398 case FEATURE_GROUPS: return true;
1399 case FEATURE_GROUPINGS: return true;
1400 case FEATURE_GROUPMEMBERSONLY: return true;
dc5c2bd9 1401 case FEATURE_MOD_INTRO: return true;
4e781c7b 1402 case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
42f103be 1403 case FEATURE_GRADE_HAS_GRADE: return true;
1404 case FEATURE_GRADE_OUTCOMES: return true;
17da2e6f 1405 case FEATURE_MOD_SUBPLUGINS: return array('quiz'=>'mod/quiz/report');
42f103be 1406
49f6e5f4 1407 default: return null;
4e781c7b 1408 }
1409}
1410
f432bebf 1411/**
8cc86111 1412 * @global object
1413 * @global stdClass
cca6e300 1414 * @return array all other caps used in module
f432bebf 1415 */
1416function quiz_get_extra_capabilities() {
be0ba083 1417 global $DB, $CFG;
1418 require_once($CFG->libdir.'/questionlib.php');
cca6e300 1419 $caps = question_get_all_capabilities();
bbf4f440 1420 $reportcaps = $DB->get_records_select_menu('capabilities', 'name LIKE ?', array('quizreport/%'), 'id,name');
1421 $caps = array_merge($caps, $reportcaps);
cca6e300 1422 $caps[] = 'moodle/site:accessallgroups';
1423 return $caps;
f432bebf 1424}
55f599f0 1425
1426/**
1427 * This fucntion extends the global navigaiton for the site.
1428 * It is important to note that you should not rely on PAGE objects within this
1429 * body of code as there is no guarantee that during an AJAX request they are
1430 * available
1431 *
1432 * @param navigation_node $navigation The quiz node within the global navigation
1433 * @param stdClass $course The course object returned from the DB
1434 * @param stdClass $module The module object returned from the DB
1435 * @param stdClass $cm The course module isntance returned from the DB
1436 */
1437function quiz_extend_navigation($navigation, $course, $module, $cm) {
1438 /**
1439 * This is currently just a stub so that it can be easily expanded upon.
1440 * When expanding just remove this comment and the line below and then add
1441 * you content.
1442 */
1443 $navigation->nodetype = navigation_node::NODETYPE_LEAF;
1444}
1445
1446/**
1447 * This function extends the settings navigation block for the site.
1448 *
1449 * It is safe to rely on PAGE here as we will only ever be within the module
1450 * context when this is called
1451 *
1452 * @param navigation_node $settings
1453 * @param stdClass $module
1454 */
1455function quiz_extend_settings_navigation($settings, $module) {
56115eea 1456 global $PAGE, $CFG, $OUTPUT;
55f599f0 1457
55f599f0 1458 $quiznavkey = $settings->add(get_string('quizadministration', 'quiz'));
1459 $quiznav = $settings->get($quiznavkey);
1460 $quiznav->forceopen = true;
1461
1462 if (has_capability('mod/quiz:view', $PAGE->cm->context)) {
a6855934 1463 $url = new moodle_url('/mod/quiz/view.php', array('id'=>$PAGE->cm->id));
f9fc1461 1464 $quiznav->add(get_string('info', 'quiz'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/info', ''));
55f599f0 1465 }
1466 if (has_capability('mod/quiz:viewreports', $PAGE->cm->context)) {
a6855934 1467 $url = new moodle_url('/mod/quiz/report.php', array('q'=>$PAGE->cm->instance));
f9fc1461 1468 $reportkey = $quiznav->add(get_string('results', 'quiz'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/report', ''));
55f599f0 1469
1470 require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php');
1471 $reportlist = quiz_report_list($PAGE->cm->context);
1472 foreach ($reportlist as $report) {
a6855934 1473 $url = new moodle_url('/mod/quiz/report.php', array('q'=>$PAGE->cm->instance, 'mode'=>$report));
f9fc1461 1474 $quiznav->get($reportkey)->add(get_string($report, 'quiz_'.$report), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/item', ''));
55f599f0 1475 }
1476 }
1477 if (has_capability('mod/quiz:preview', $PAGE->cm->context)) {
a6855934 1478 $url = new moodle_url('/mod/quiz/startattempt.php', array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey()));
f9fc1461 1479 $quiznav->add(get_string('preview', 'quiz'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('t/preview', ''));
55f599f0 1480 }
1481 if (has_capability('mod/quiz:manage', $PAGE->cm->context)) {
a6855934 1482 $url = new moodle_url('/mod/quiz/edit.php', array('cmid'=>$PAGE->cm->id));
f9fc1461 1483 $quiznav->add(get_string('edit'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('t/edit', ''));
55f599f0 1484 }
1485
1486 if (has_capability('moodle/course:manageactivities', $PAGE->cm->context)) {
a6855934 1487 $url = new moodle_url('/course/mod.php', array('update' => $PAGE->cm->id, 'return' => true, 'sesskey' => sesskey()));
55f599f0 1488 $quiznav->add(get_string('updatethis', '', get_string('modulename', 'quiz')), $url);
1489 }
1490
1491 if (count($quiznav->children)<1) {
1492 $settings->remove_child($quiznavkey);
1493 }
1a96363a 1494}