MDL-18910 moving modedit features to modname_supports()
[moodle.git] / mod / quiz / lib.php
CommitLineData
d1290cec 1<?php // $Id$
84e628a0 2
3///////////////////////////////////////////////////////////////////////////
4// //
5// NOTICE OF COPYRIGHT //
6// //
7// Moodle - Modular Object-Oriented Dynamic Learning Environment //
8// http://moodle.org //
9// //
10// Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
11// //
12// This program is free software; you can redistribute it and/or modify //
13// it under the terms of the GNU General Public License as published by //
14// the Free Software Foundation; either version 2 of the License, or //
15// (at your option) any later version. //
16// //
17// This program is distributed in the hope that it will be useful, //
18// but WITHOUT ANY WARRANTY; without even the implied warranty of //
19// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
20// GNU General Public License for more details: //
21// //
22// http://www.gnu.org/copyleft/gpl.html //
23// //
24///////////////////////////////////////////////////////////////////////////
25
ee1fb969 26/**
84e628a0 27 * Library of functions for the quiz module.
28 *
29 * This contains functions that are called also from outside the quiz module
30 * Functions that are only called by the quiz module itself are in {@link locallib.php}
31 *
32 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
33 * @package quiz
34 */
730fd187 35
84e628a0 36require_once($CFG->libdir . '/pagelib.php');
37require_once($CFG->libdir . '/eventslib.php');
8966a111 38
75cd257b 39/// CONSTANTS ///////////////////////////////////////////////////////////////////
40
e2249afe 41/**#@+
42 * Options determining how the grades from individual attempts are combined to give
43 * the overall grade for a user
44 */
84e628a0 45define('QUIZ_GRADEHIGHEST', 1);
46define('QUIZ_GRADEAVERAGE', 2);
47define('QUIZ_ATTEMPTFIRST', 3);
48define('QUIZ_ATTEMPTLAST', 4);
e2249afe 49/**#@-*/
50
84e628a0 51define('QUIZ_MAX_ATTEMPT_OPTION', 10);
52define('QUIZ_MAX_QPP_OPTION', 50);
53define('QUIZ_MAX_DECIMAL_OPTION', 5);
54define('QUIZ_MAX_Q_DECIMAL_OPTION', 7);
55
75cd257b 56/**#@+
57 * The different review options are stored in the bits of $quiz->review
58 * These constants help to extract the options
b159da78 59 *
00719c02 60 * This is more of a mess than you might think necessary, because originally
61 * it was though that 3x6 bits were enough, but then they ran out. PHP integers
62 * are only reliably 32 bits signed, so the simplest solution was then to
b159da78 63 * add 4x3 more bits.
75cd257b 64 */
65/**
00719c02 66 * The first 6 + 4 bits refer to the time immediately after the attempt
75cd257b 67 */
00719c02 68define('QUIZ_REVIEW_IMMEDIATELY', 0x3c003f);
75cd257b 69/**
00719c02 70 * the next 6 + 4 bits refer to the time after the attempt but while the quiz is open
75cd257b 71 */
00719c02 72define('QUIZ_REVIEW_OPEN', 0x3c00fc0);
75cd257b 73/**
00719c02 74 * the final 6 + 4 bits refer to the time after the quiz closes
75cd257b 75 */
00719c02 76define('QUIZ_REVIEW_CLOSED', 0x3c03f000);
75cd257b 77
78// within each group of 6 bits we determine what should be shown
00719c02 79define('QUIZ_REVIEW_RESPONSES', 1*0x1041); // Show responses
80define('QUIZ_REVIEW_SCORES', 2*0x1041); // Show scores
81define('QUIZ_REVIEW_FEEDBACK', 4*0x1041); // Show question feedback
82define('QUIZ_REVIEW_ANSWERS', 8*0x1041); // Show correct answers
75cd257b 83// Some handling of worked solutions is already in the code but not yet fully supported
84// and not switched on in the user interface.
00719c02 85define('QUIZ_REVIEW_SOLUTIONS', 16*0x1041); // Show solutions
86define('QUIZ_REVIEW_GENERALFEEDBACK',32*0x1041); // Show question general feedback
87define('QUIZ_REVIEW_OVERALLFEEDBACK', 1*0x4440000); // Show quiz overall feedback
88// Multipliers 2*0x4440000, 4*0x4440000 and 8*0x4440000 are still available
75cd257b 89/**#@-*/
90
91/**
92 * If start and end date for the quiz are more than this many seconds apart
93 * they will be represented by two separate events in the calendar
94 */
00719c02 95define("QUIZ_MAX_EVENT_LENGTH", 5*24*60*60); // 5 days maximum
ee1fb969 96
a5e1f35c 97/// FUNCTIONS ///////////////////////////////////////////////////////////////////
730fd187 98
920b93d1 99/**
100 * Given an object containing all the necessary data,
7cac0c4b 101 * (defined by the form in mod_form.php) this function
920b93d1 102 * will create a new instance and return the id number
103 * of the new instance.
a23f0aaf 104 *
920b93d1 105 * @param object $quiz the data that came from the form.
212b7b8c 106 * @return mixed the id of the new instance on success,
107 * false or a string error message on failure.
920b93d1 108 */
730fd187 109function quiz_add_instance($quiz) {
c18269c7 110 global $DB;
730fd187 111
920b93d1 112 // Process the options from the form.
113 $quiz->created = time();
bc569413 114 $quiz->questions = '';
212b7b8c 115 $result = quiz_process_options($quiz);
116 if ($result && is_string($result)) {
117 return $result;
118 }
6f797013 119
920b93d1 120 // Try to store it in the database.
eeab18f0 121 $quiz->id = $DB->insert_record('quiz', $quiz);
7bd1aa1d 122
920b93d1 123 // Do the processing required after an add or an update.
124 quiz_after_add_or_update($quiz);
a23f0aaf 125
7bd1aa1d 126 return $quiz->id;
730fd187 127}
128
920b93d1 129/**
130 * Given an object containing all the necessary data,
7cac0c4b 131 * (defined by the form in mod_form.php) this function
920b93d1 132 * will update an existing instance with new data.
a23f0aaf 133 *
920b93d1 134 * @param object $quiz the data that came from the form.
212b7b8c 135 * @return mixed true on success, false or a string error message on failure.
920b93d1 136 */
eeab18f0 137function quiz_update_instance($quiz, $mform) {
138 global $CFG, $DB;
730fd187 139
920b93d1 140 // Process the options from the form.
212b7b8c 141 $result = quiz_process_options($quiz);
142 if ($result && is_string($result)) {
143 return $result;
144 }
ee1fb969 145
eeab18f0 146 // Repaginate, if asked to.
147 if (!$quiz->shufflequestions && !empty($quiz->repaginatenow)) {
148 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
149 $quiz->questions = $DB->get_field('quiz', 'questions', array('id' => $quiz->instance));
150 $quiz->questions = quiz_repaginate($quiz->questions, $quiz->questionsperpage);
151 }
152 unset($quiz->repaginatenow);
153
920b93d1 154 // Update the database.
730fd187 155 $quiz->id = $quiz->instance;
eeab18f0 156 $DB->update_record('quiz', $quiz);
730fd187 157
920b93d1 158 // Do the processing required after an add or an update.
159 quiz_after_add_or_update($quiz);
ee1fb969 160
920b93d1 161 // Delete any previous preview attempts
53004e48 162 quiz_delete_previews($quiz);
d2f308c0 163
7bd1aa1d 164 return true;
730fd187 165}
166
730fd187 167function quiz_delete_instance($id) {
c18269c7 168 global $DB;
f41e824f 169/// Given an ID of an instance of this module,
170/// this function will permanently delete the instance
171/// and any data that depends on it.
730fd187 172
53004e48 173 if (!$quiz = $DB->get_record('quiz', array('id' => $id))) {
730fd187 174 return false;
175 }
176
53004e48 177 quiz_delete_all_attempts($quiz);
730fd187 178
53004e48 179 $DB->delete_records('quiz_question_instances', array('quiz' => $quiz->id));
180 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
730fd187 181
880d8675 182 $pagetypes = page_import_types('mod/quiz/');
183 foreach($pagetypes as $pagetype) {
53004e48 184 blocks_delete_all_on_page($pagetype, $quiz->id);
880d8675 185 }
186
53004e48 187 $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id));
188 foreach($events as $event) {
189 delete_event($event->id);
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.
200 * @param $quiz The quiz object.
201 */
202function quiz_delete_all_attempts($quiz) {
203 global $CFG, $DB;
204 require_once($CFG->libdir . '/questionlib.php');
205 $attempts = $DB->get_records('quiz_attempts', array('quiz' => $quiz->id));
206 foreach ($attempts as $attempt) {
207 delete_attempt($attempt->uniqueid);
208 }
209 $DB->delete_records('quiz_attempts', array('quiz' => $quiz->id));
210 $DB->delete_records('quiz_grades', array('quiz' => $quiz->id));
730fd187 211}
212
213function quiz_user_outline($course, $user, $mod, $quiz) {
9cf4a18b 214 global $DB;
f41e824f 215/// Return a small object with summary information about what a
a5e1f35c 216/// user has done with a given particular instance of this module
217/// Used for user activity reports.
218/// $return->time = the time they did it
219/// $return->info = a short text description
5ecfab51 220 $grade = quiz_get_best_grade($quiz, $user->id);
221 if (is_null($grade)) {
222 return NULL;
223 }
224
225 $result = new stdClass;
226 $result->info = get_string('grade') . ': ' . $grade . '/' . $quiz->grade;
72b9b786 227 $result->time = $DB->get_field('quiz_attempts', 'MAX(timefinish)', array('userid' => $user->id, 'quiz' => $quiz->id));
5ecfab51 228 return $result;
98092498 229 }
730fd187 230
739b0711 231/**
232 * Is this a graded quiz? If this method returns true, you can assume that
233 * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to
234 * divide by them).
235 *
236 * @param object $quiz a row from the quiz table.
237 * @return boolean whether this is a graded quiz.
238 */
239function quiz_has_grades($quiz) {
240 return $quiz->grade != 0 && $quiz->sumgrades != 0;
241}
242
40f6f97f 243/**
244 * Get the best current grade for a particular user in a quiz.
245 *
246 * @param object $quiz the quiz object.
247 * @param integer $userid the id of the user.
248 * @return float the user's current grade for this quiz, or NULL if this user does
249 * not have a grade on this quiz.
250 */
251function quiz_get_best_grade($quiz, $userid) {
252 global $DB;
253 $grade = $DB->get_field('quiz_grades', 'grade', array('quiz' => $quiz->id, 'userid' => $userid));
254
255 // Need to detect errors/no result, without catching 0 scores.
256 if (is_numeric($grade)) {
257 return quiz_format_grade($quiz, $grade);
258 } else {
259 return NULL;
260 }
261}
262
730fd187 263function quiz_user_complete($course, $user, $mod, $quiz) {
9cf4a18b 264 global $DB;
f41e824f 265/// Print a detailed representation of what a user has done with
a5e1f35c 266/// a given particular instance of this module, for user activity reports.
730fd187 267
5ecfab51 268 if ($attempts = $DB->get_records('quiz_attempts', array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) {
739b0711 269 if (quiz_has_grades($quiz) && $grade = quiz_get_best_grade($quiz, $user->id)) {
5ecfab51 270 echo get_string('grade') . ': ' . $grade . '/' . quiz_format_grade($quiz, $quiz->grade) . '<br />';
ee1fb969 271 }
272 foreach ($attempts as $attempt) {
273 echo get_string('attempt', 'quiz').' '.$attempt->attempt.': ';
274 if ($attempt->timefinish == 0) {
275 print_string('unfinished');
276 } else {
84e628a0 277 echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' . quiz_format_grade($quiz, $quiz->sumgrades);
ee1fb969 278 }
279 echo ' - '.userdate($attempt->timemodified).'<br />';
280 }
281 } else {
282 print_string('noattempts', 'quiz');
283 }
284
730fd187 285 return true;
286}
287
be0ba083 288function quiz_cron() {
a5e1f35c 289/// Function to be run periodically according to the moodle cron
f41e824f 290/// This function searches for things that need to be done, such
291/// as sending out mail, toggling flags etc ...
730fd187 292
293 global $CFG;
294
295 return true;
296}
297
b5a16eb7 298/**
299 * @param integer $quizid the quiz id.
300 * @param integer $userid the userid.
301 * @param string $status 'all', 'finished' or 'unfinished' to control
302 * @return an array of all the user's attempts at this quiz. Returns an empty array if there are none.
303 */
98f38217 304function quiz_get_user_attempts($quizid, $userid=0, $status = 'finished', $includepreviews = false) {
9cf4a18b 305 global $DB;
b5a16eb7 306 $status_condition = array(
307 'all' => '',
308 'finished' => ' AND timefinish > 0',
309 'unfinished' => ' AND timefinish = 0'
310 );
311 $previewclause = '';
312 if (!$includepreviews) {
313 $previewclause = ' AND preview = 0';
314 }
98f38217 315 $params=array($quizid);
316 if ($userid){
317 $userclause = ' AND userid = ?';
318 $params[]=$userid;
319 } else {
320 $userclause = '';
321 }
9cf4a18b 322 if ($attempts = $DB->get_records_select('quiz_attempts',
98f38217 323 "quiz = ?" .$userclause. $previewclause . $status_condition[$status], $params,
b5a16eb7 324 'attempt ASC')) {
325 return $attempts;
326 } else {
327 return array();
328 }
329}
858deff0 330
d6dd2108 331/**
332 * Return grade for given user or all users.
333 *
334 * @param int $quizid id of quiz
335 * @param int $userid optional user id, 0 means all users
f88fb62c 336 * @return array array of grades, false if none. These are raw grades. They should
337 * be processed with quiz_format_grade for display.
d6dd2108 338 */
339function quiz_get_user_grades($quiz, $userid=0) {
9cf4a18b 340 global $CFG, $DB;
d6dd2108 341
9cf4a18b 342 $params = array($quiz->id);
343 $wheresql = '';
344 if ($userid) {
345 $params[] = $userid;
346 $wheresql = "AND u.id = ?";
347 }
d82b018f 348 $sql = "SELECT u.id, u.id AS userid, g.grade AS rawgrade, g.timemodified AS dategraded, MAX(a.timefinish) AS datesubmitted
9cf4a18b 349 FROM {user} u, {quiz_grades} g, {quiz_attempts} a
350 WHERE u.id = g.userid AND g.quiz = ? AND a.quiz = g.quiz AND u.id = a.userid $wheresql
d82b018f 351 GROUP BY u.id, g.grade, g.timemodified";
352
9cf4a18b 353 return $DB->get_records_sql($sql, $params);
d6dd2108 354}
355
f88fb62c 356/**
357 * Round a grade to to the correct number of decimal places, and format it for display.
358 *
359 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
360 * @param float $grade The grade to round.
361 */
362function quiz_format_grade($quiz, $grade) {
363 return format_float($grade, $quiz->decimalpoints);
364}
365
84e628a0 366/**
367 * Round a grade to to the correct number of decimal places, and format it for display.
368 *
369 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
370 * @param float $grade The grade to round.
371 */
372function quiz_format_question_grade($quiz, $grade) {
373 if ($quiz->questiondecimalpoints == -1) {
374 return format_float($grade, $quiz->decimalpoints);
375 } else {
376 return format_float($grade, $quiz->questiondecimalpoints);
377 }
378}
379
d6dd2108 380/**
381 * Update grades in central gradebook
382 *
775f811a 383 * @param object $quiz
384 * @param int $userid specific user only, 0 means all
d6dd2108 385 */
775f811a 386function quiz_update_grades($quiz, $userid=0, $nullifnone=true) {
9cf4a18b 387 global $CFG, $DB;
775f811a 388 require_once($CFG->libdir.'/gradelib.php');
ed1daaa9 389
775f811a 390 if ($quiz->grade == 0) {
391 quiz_grade_item_update($quiz);
d6dd2108 392
775f811a 393 } else if ($grades = quiz_get_user_grades($quiz, $userid)) {
394 quiz_grade_item_update($quiz, $grades);
eafb9d9e 395
775f811a 396 } else if ($userid and $nullifnone) {
397 $grade = new object();
398 $grade->userid = $userid;
399 $grade->rawgrade = NULL;
400 quiz_grade_item_update($quiz, $grade);
d6dd2108 401
402 } else {
775f811a 403 quiz_grade_item_update($quiz);
404 }
405}
406
407/**
408 * Update all grades in gradebook.
409 */
410function quiz_upgrade_grades() {
411 global $DB;
412
413 $sql = "SELECT COUNT('x')
414 FROM {quiz} a, {course_modules} cm, {modules} m
415 WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
416 $count = $DB->count_records_sql($sql);
417
418 $sql = "SELECT a.*, cm.idnumber AS cmidnumber, a.course AS courseid
419 FROM {quiz} a, {course_modules} cm, {modules} m
420 WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
421 if ($rs = $DB->get_recordset_sql($sql)) {
775f811a 422 $pbar = new progress_bar('quizupgradegrades', 500, true);
423 $i=0;
424 foreach ($rs as $quiz) {
425 $i++;
426 upgrade_set_timeout(60*5); // set up timeout, may also abort execution
427 quiz_update_grades($quiz, 0, false);
428 $pbar->update($i, $count, "Updating Quiz grades ($i/$count).");
d6dd2108 429 }
775f811a 430 $rs->close();
d6dd2108 431 }
d0ac6bc2 432}
433
d6dd2108 434/**
435 * Create grade item for given quiz
436 *
437 * @param object $quiz object with extra cmidnumber
0b5a80a1 438 * @param mixed optional array/object of grade(s); 'reset' means reset grades in gradebook
d6dd2108 439 * @return int 0 if ok, error code otherwise
440 */
ced5ee59 441function quiz_grade_item_update($quiz, $grades=NULL) {
d6dd2108 442 global $CFG;
443 if (!function_exists('grade_update')) { //workaround for buggy PHP versions
444 require_once($CFG->libdir.'/gradelib.php');
445 }
446
447 if (array_key_exists('cmidnumber', $quiz)) { //it may not be always present
448 $params = array('itemname'=>$quiz->name, 'idnumber'=>$quiz->cmidnumber);
449 } else {
450 $params = array('itemname'=>$quiz->name);
451 }
452
453 if ($quiz->grade > 0) {
454 $params['gradetype'] = GRADE_TYPE_VALUE;
455 $params['grademax'] = $quiz->grade;
456 $params['grademin'] = 0;
457
458 } else {
459 $params['gradetype'] = GRADE_TYPE_NONE;
460 }
461
1223d24a 462/* description by TJ:
4631/ If the quiz is set to not show scores while the quiz is still open, and is set to show scores after
464 the quiz is closed, then create the grade_item with a show-after date that is the quiz close date.
4652/ If the quiz is set to not show scores at either of those times, create the grade_item as hidden.
4663/ If the quiz is set to show scores, create the grade_item visible.
467*/
468 if (!($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
469 and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
470 $params['hidden'] = 1;
471
472 } else if ( ($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
473 and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
474 if ($quiz->timeclose) {
475 $params['hidden'] = $quiz->timeclose;
476 } else {
477 $params['hidden'] = 1;
478 }
479
480 } else {
481 // a) both open and closed enabled
482 // b) open enabled, closed disabled - we can not "hide after", grades are kept visible even after closing
483 $params['hidden'] = 0;
484 }
485
0b5a80a1 486 if ($grades === 'reset') {
487 $params['reset'] = true;
488 $grades = NULL;
489 }
9cf4a18b 490
49460d84 491 $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
d45459b7 492 if (!empty($gradebook_grades->items)) {
493 $grade_item = $gradebook_grades->items[0];
494 if ($grade_item->locked) {
495 $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
496 if (!$confirm_regrade) {
497 $message = get_string('gradeitemislocked', 'grades');
498 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id . '&amp;mode=overview';
499 $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
500 print_box_start('generalbox', 'notice');
501 echo '<p>'. $message .'</p>';
502 echo '<div class="buttons">';
503 print_single_button($regrade_link, null, get_string('regradeanyway', 'grades'), 'post', $CFG->framename);
504 print_single_button($back_link, null, get_string('cancel'), 'post', $CFG->framename);
505 echo '</div>';
506 print_box_end();
9cf4a18b 507
d45459b7 508 return GRADE_UPDATE_ITEM_LOCKED;
509 }
49460d84 510 }
511 }
0b5a80a1 512
ced5ee59 513 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
d6dd2108 514}
515
516/**
517 * Delete grade item for given quiz
518 *
519 * @param object $quiz object
520 * @return object quiz
521 */
522function quiz_grade_item_delete($quiz) {
523 global $CFG;
53004e48 524 require_once($CFG->libdir . '/gradelib.php');
d6dd2108 525
53004e48 526 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, NULL, array('deleted' => 1));
d6dd2108 527}
528
e2249afe 529/**
530 * @return the options for calculating the quiz grade from the individual attempt grades.
531 */
532function quiz_get_grading_options() {
533 return array (
534 QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'),
535 QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'),
536 QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'),
537 QUIZ_ATTEMPTLAST => get_string('attemptlast', 'quiz'));
538}
d6dd2108 539
d061d883 540function quiz_get_participants($quizid) {
541/// Returns an array of users who have data in a given quiz
9cf4a18b 542 global $CFG, $DB;
d061d883 543
e4acc4ce 544 //Get users from attempts
9cf4a18b 545 $us_attempts = $DB->get_records_sql("SELECT DISTINCT u.id, u.id
546 FROM {user} u,
547 {quiz_attempts} a
548 WHERE a.quiz = ? and
549 u.id = a.userid", array($quizid));
e4acc4ce 550
e4acc4ce 551 //Return us_attempts array (it contains an array of unique users)
6b224376 552 return $us_attempts;
e4acc4ce 553
d061d883 554}
730fd187 555
d2f308c0 556function quiz_refresh_events($courseid = 0) {
9cf4a18b 557 global $DB;
d2f308c0 558// This standard function will check all instances of this module
559// and make sure there are up-to-date events created for each of them.
b2a3cd2d 560// If courseid = 0, then every quiz event in the site is checked, else
561// only quiz events belonging to the course specified are checked.
d2f308c0 562// This function is used, in its new format, by restore_refresh_events()
563
564 if ($courseid == 0) {
9cf4a18b 565 if (! $quizzes = $DB->get_records('quiz')) {
d2f308c0 566 return true;
567 }
568 } else {
9cf4a18b 569 if (! $quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
d2f308c0 570 return true;
571 }
572 }
9cf4a18b 573 $moduleid = $DB->get_field('modules', 'id', array('name' => 'quiz'));
f41e824f 574
d2f308c0 575 foreach ($quizzes as $quiz) {
576 $event = NULL;
b2a3cd2d 577 $event2 = NULL;
578 $event2old = NULL;
579
53004e48 580 if ($events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id), 'timestart')) {
b2a3cd2d 581 $event = array_shift($events);
582 if (!empty($events)) {
583 $event2old = array_shift($events);
584 if (!empty($events)) {
585 foreach ($events as $badevent) {
53004e48 586 delete_event($badevent->id);
b2a3cd2d 587 }
588 }
589 }
590 }
591
ae040d4b 592 $event->name = $quiz->name;
593 $event->description = $quiz->intro;
b2a3cd2d 594 $event->courseid = $quiz->course;
595 $event->groupid = 0;
596 $event->userid = 0;
597 $event->modulename = 'quiz';
598 $event->instance = $quiz->id;
ba288539 599 $event->visible = instance_is_visible('quiz', $quiz);
d2f308c0 600 $event->timestart = $quiz->timeopen;
b2a3cd2d 601 $event->eventtype = 'open';
d2f308c0 602 $event->timeduration = ($quiz->timeclose - $quiz->timeopen);
d2f308c0 603
b2a3cd2d 604 if ($event->timeduration > QUIZ_MAX_EVENT_LENGTH) { /// Set up two events
d2f308c0 605
b2a3cd2d 606 $event2 = $event;
d2f308c0 607
ae040d4b 608 $event->name = $quiz->name.' ('.get_string('quizopens', 'quiz').')';
b2a3cd2d 609 $event->timeduration = 0;
610
ae040d4b 611 $event2->name = $quiz->name.' ('.get_string('quizcloses', 'quiz').')';
b2a3cd2d 612 $event2->timestart = $quiz->timeclose;
613 $event2->eventtype = 'close';
614 $event2->timeduration = 0;
615
616 if (empty($event2old->id)) {
617 unset($event2->id);
618 add_event($event2);
619 } else {
620 $event2->id = $event2old->id;
621 update_event($event2);
622 }
acda04bf 623 } else if (!empty($event2old->id)) {
624 delete_event($event2old->id);
b2a3cd2d 625 }
626
627 if (empty($event->id)) {
acda04bf 628 if (!empty($event->timestart)) {
629 add_event($event);
630 }
b2a3cd2d 631 } else {
632 update_event($event);
d2f308c0 633 }
b2a3cd2d 634
d2f308c0 635 }
636 return true;
637}
638
dd97c328 639/**
640 * Returns all quiz graded users since a given time for specified quiz
641 */
642function quiz_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid=0, $groupid=0) {
9cf4a18b 643 global $CFG, $COURSE, $USER, $DB;
6710ec87 644
dd97c328 645 if ($COURSE->id == $courseid) {
646 $course = $COURSE;
6710ec87 647 } else {
9cf4a18b 648 $course = $DB->get_record('course', array('id' => $courseid));
6710ec87 649 }
6710ec87 650
dd97c328 651 $modinfo =& get_fast_modinfo($course);
6710ec87 652
dd97c328 653 $cm = $modinfo->cms[$cmid];
6710ec87 654
9cf4a18b 655 $params = array($timestart, $cm->instance);
656
dd97c328 657 if ($userid) {
9cf4a18b 658 $userselect = "AND u.id = ?";
659 $params[] = $userid;
dd97c328 660 } else {
661 $userselect = "";
662 }
ac21ad39 663
dd97c328 664 if ($groupid) {
9cf4a18b 665 $groupselect = "AND gm.groupid = ?";
666 $groupjoin = "JOIN {groups_members} gm ON gm.userid=u.id";
667 $params[] = $groupid;
dd97c328 668 } else {
669 $groupselect = "";
670 $groupjoin = "";
671 }
6710ec87 672
9cf4a18b 673 if (!$attempts = $DB->get_records_sql("SELECT qa.*, q.sumgrades AS maxgrade,
674 u.firstname, u.lastname, u.email, u.picture
675 FROM {quiz_attempts} qa
676 JOIN {quiz} q ON q.id = qa.quiz
677 JOIN {user} u ON u.id = qa.userid
dd97c328 678 $groupjoin
679 WHERE qa.timefinish > $timestart AND q.id = $cm->instance
680 $userselect $groupselect
9cf4a18b 681 ORDER BY qa.timefinish ASC", $params)) {
dd97c328 682 return;
683 }
6710ec87 684
dd97c328 685 $cm_context = get_context_instance(CONTEXT_MODULE, $cm->id);
686 $grader = has_capability('moodle/grade:viewall', $cm_context);
687 $accessallgroups = has_capability('moodle/site:accessallgroups', $cm_context);
688 $viewfullnames = has_capability('moodle/site:viewfullnames', $cm_context);
689 $grader = has_capability('mod/quiz:grade', $cm_context);
690 $groupmode = groups_get_activity_groupmode($cm, $course);
6710ec87 691
dd97c328 692 if (is_null($modinfo->groups)) {
693 $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
694 }
6710ec87 695
dd97c328 696 $aname = format_string($cm->name,true);
697 foreach ($attempts as $attempt) {
698 if ($attempt->userid != $USER->id) {
699 if (!$grader) {
700 // grade permission required
701 continue;
702 }
6710ec87 703
9cf4a18b 704 if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
dd97c328 705 $usersgroups = groups_get_all_groups($course->id, $attempt->userid, $cm->groupingid);
706 if (!is_array($usersgroups)) {
707 continue;
708 }
709 $usersgroups = array_keys($usersgroups);
710 $interset = array_intersect($usersgroups, $modinfo->groups[$cm->id]);
711 if (empty($intersect)) {
712 continue;
713 }
714 }
715 }
716
717 $tmpactivity = new object();
718
719 $tmpactivity->type = 'quiz';
720 $tmpactivity->cmid = $cm->id;
721 $tmpactivity->name = $aname;
76cbde41 722 $tmpactivity->sectionnum= $cm->sectionnum;
dd97c328 723 $tmpactivity->timestamp = $attempt->timefinish;
9cf4a18b 724
dd97c328 725 $tmpactivity->content->attemptid = $attempt->id;
726 $tmpactivity->content->sumgrades = $attempt->sumgrades;
727 $tmpactivity->content->maxgrade = $attempt->maxgrade;
728 $tmpactivity->content->attempt = $attempt->attempt;
9cf4a18b 729
dd97c328 730 $tmpactivity->user->userid = $attempt->userid;
731 $tmpactivity->user->fullname = fullname($attempt, $viewfullnames);
732 $tmpactivity->user->picture = $attempt->picture;
9cf4a18b 733
dd97c328 734 $activities[$index++] = $tmpactivity;
6710ec87 735 }
736
737 return;
738}
739
dd97c328 740function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
6eaae5bd 741 global $CFG;
6710ec87 742
dd97c328 743 echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">';
6710ec87 744
dd97c328 745 echo "<tr><td class=\"userpicture\" valign=\"top\">";
746 print_user_picture($activity->user->userid, $courseid, $activity->user->picture);
747 echo "</td><td>";
6710ec87 748
749 if ($detail) {
dd97c328 750 $modname = $modnames[$activity->type];
751 echo '<div class="title">';
752 echo "<img src=\"$CFG->modpixpath/{$activity->type}/icon.gif\" ".
753 "class=\"icon\" alt=\"$modname\" />";
754 echo "<a href=\"$CFG->wwwroot/mod/quiz/view.php?id={$activity->cmid}\">{$activity->name}</a>";
755 echo '</div>';
6710ec87 756 }
757
dd97c328 758 echo '<div class="grade">';
759 echo get_string("attempt", "quiz")." {$activity->content->attempt}: ";
760 $grades = "({$activity->content->sumgrades} / {$activity->content->maxgrade})";
761 echo "<a href=\"$CFG->wwwroot/mod/quiz/review.php?attempt={$activity->content->attemptid}\">$grades</a>";
762 echo '</div>';
6710ec87 763
dd97c328 764 echo '<div class="user">';
765 echo "<a href=\"$CFG->wwwroot/user/view.php?id={$activity->user->userid}&amp;course=$courseid\">"
766 ."{$activity->user->fullname}</a> - ".userdate($activity->timestamp);
767 echo '</div>';
6710ec87 768
dd97c328 769 echo "</td></tr></table>";
6710ec87 770
771 return;
772}
773
ee1fb969 774/**
920b93d1 775 * Pre-process the quiz options form data, making any necessary adjustments.
ad4cd837 776 * Called by add/update instance in this file.
b159da78 777 *
920b93d1 778 * @param object $quiz The variables set on the form.
779 */
780function quiz_process_options(&$quiz) {
781 $quiz->timemodified = time();
ee1fb969 782
dc5c6851 783 // Quiz name.
784 if (!empty($quiz->name)) {
785 $quiz->name = trim($quiz->name);
786 }
a23f0aaf 787
ab0a8ff2 788 // Password field - different in form to stop browsers that remember passwords
789 // getting confused.
790 $quiz->password = $quiz->quizpassword;
791 unset($quiz->quizpassword);
792
212b7b8c 793 // Quiz feedback
a0807a00 794 if (isset($quiz->feedbacktext)) {
795 // Clean up the boundary text.
796 for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
797 if (empty($quiz->feedbacktext[$i])) {
798 $quiz->feedbacktext[$i] = '';
799 } else {
800 $quiz->feedbacktext[$i] = trim($quiz->feedbacktext[$i]);
801 }
212b7b8c 802 }
b159da78 803
a0807a00 804 // Check the boundary value is a number or a percentage, and in range.
805 $i = 0;
806 while (!empty($quiz->feedbackboundaries[$i])) {
807 $boundary = trim($quiz->feedbackboundaries[$i]);
808 if (!is_numeric($boundary)) {
809 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
810 $boundary = trim(substr($boundary, 0, -1));
811 if (is_numeric($boundary)) {
812 $boundary = $boundary * $quiz->grade / 100.0;
813 } else {
814 return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
815 }
212b7b8c 816 }
817 }
a0807a00 818 if ($boundary <= 0 || $boundary >= $quiz->grade) {
819 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
820 }
821 if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
822 return get_string('feedbackerrororder', 'quiz', $i + 1);
823 }
824 $quiz->feedbackboundaries[$i] = $boundary;
825 $i += 1;
212b7b8c 826 }
a0807a00 827 $numboundaries = $i;
b159da78 828
a0807a00 829 // Check there is nothing in the remaining unused fields.
e0b7cfcb 830 if (!empty($quiz->feedbackboundaries)) {
831 for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
832 if (!empty($quiz->feedbackboundaries[$i]) && trim($quiz->feedbackboundaries[$i]) != '') {
833 return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
834 }
a0807a00 835 }
212b7b8c 836 }
a0807a00 837 for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
838 if (!empty($quiz->feedbacktext[$i]) && trim($quiz->feedbacktext[$i]) != '') {
839 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
840 }
212b7b8c 841 }
a0807a00 842 $quiz->feedbackboundaries[-1] = $quiz->grade + 1; // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
843 $quiz->feedbackboundaries[$numboundaries] = 0;
844 $quiz->feedbackboundarycount = $numboundaries;
212b7b8c 845 }
a23f0aaf 846
920b93d1 847 // Settings that get combined to go into the optionflags column.
848 $quiz->optionflags = 0;
849 if (!empty($quiz->adaptive)) {
850 $quiz->optionflags |= QUESTION_ADAPTIVE;
851 }
852
853 // Settings that get combined to go into the review column.
854 $review = 0;
855 if (isset($quiz->responsesimmediately)) {
ee1fb969 856 $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 857 unset($quiz->responsesimmediately);
ee1fb969 858 }
920b93d1 859 if (isset($quiz->responsesopen)) {
ee1fb969 860 $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN);
920b93d1 861 unset($quiz->responsesopen);
ee1fb969 862 }
920b93d1 863 if (isset($quiz->responsesclosed)) {
ee1fb969 864 $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED);
920b93d1 865 unset($quiz->responsesclosed);
ee1fb969 866 }
867
920b93d1 868 if (isset($quiz->scoreimmediately)) {
ee1fb969 869 $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 870 unset($quiz->scoreimmediately);
ee1fb969 871 }
920b93d1 872 if (isset($quiz->scoreopen)) {
ee1fb969 873 $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN);
920b93d1 874 unset($quiz->scoreopen);
ee1fb969 875 }
920b93d1 876 if (isset($quiz->scoreclosed)) {
ee1fb969 877 $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED);
920b93d1 878 unset($quiz->scoreclosed);
ee1fb969 879 }
880
920b93d1 881 if (isset($quiz->feedbackimmediately)) {
ee1fb969 882 $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 883 unset($quiz->feedbackimmediately);
ee1fb969 884 }
920b93d1 885 if (isset($quiz->feedbackopen)) {
ee1fb969 886 $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN);
920b93d1 887 unset($quiz->feedbackopen);
ee1fb969 888 }
920b93d1 889 if (isset($quiz->feedbackclosed)) {
ee1fb969 890 $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED);
920b93d1 891 unset($quiz->feedbackclosed);
ee1fb969 892 }
893
920b93d1 894 if (isset($quiz->answersimmediately)) {
ee1fb969 895 $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 896 unset($quiz->answersimmediately);
ee1fb969 897 }
920b93d1 898 if (isset($quiz->answersopen)) {
ee1fb969 899 $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN);
920b93d1 900 unset($quiz->answersopen);
ee1fb969 901 }
920b93d1 902 if (isset($quiz->answersclosed)) {
ee1fb969 903 $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED);
920b93d1 904 unset($quiz->answersclosed);
ee1fb969 905 }
906
920b93d1 907 if (isset($quiz->solutionsimmediately)) {
ee1fb969 908 $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_IMMEDIATELY);
920b93d1 909 unset($quiz->solutionsimmediately);
ee1fb969 910 }
920b93d1 911 if (isset($quiz->solutionsopen)) {
ee1fb969 912 $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_OPEN);
920b93d1 913 unset($quiz->solutionsopen);
ee1fb969 914 }
920b93d1 915 if (isset($quiz->solutionsclosed)) {
ee1fb969 916 $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_CLOSED);
920b93d1 917 unset($quiz->solutionsclosed);
ee1fb969 918 }
919
a4514d91 920 if (isset($quiz->generalfeedbackimmediately)) {
921 $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
00719c02 922 unset($quiz->generalfeedbackimmediately);
1b8a7434 923 }
a4514d91 924 if (isset($quiz->generalfeedbackopen)) {
925 $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_OPEN);
00719c02 926 unset($quiz->generalfeedbackopen);
1b8a7434 927 }
a4514d91 928 if (isset($quiz->generalfeedbackclosed)) {
929 $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_CLOSED);
00719c02 930 unset($quiz->generalfeedbackclosed);
931 }
932
933 if (isset($quiz->overallfeedbackimmediately)) {
934 $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
935 unset($quiz->overallfeedbackimmediately);
936 }
937 if (isset($quiz->overallfeedbackopen)) {
938 $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_OPEN);
939 unset($quiz->overallfeedbackopen);
940 }
941 if (isset($quiz->overallfeedbackclosed)) {
942 $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_CLOSED);
943 unset($quiz->overallfeedbackclosed);
1b8a7434 944 }
945
920b93d1 946 $quiz->review = $review;
947}
948
949/**
950 * This function is called at the end of quiz_add_instance
951 * and quiz_update_instance, to do the common processing.
a23f0aaf 952 *
920b93d1 953 * @param object $quiz the quiz object.
954 */
955function quiz_after_add_or_update($quiz) {
c18269c7 956 global $DB;
920b93d1 957
212b7b8c 958 // Save the feedback
53004e48 959 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
a23f0aaf 960
212b7b8c 961 for ($i = 0; $i <= $quiz->feedbackboundarycount; $i += 1) {
962 $feedback = new stdClass;
963 $feedback->quizid = $quiz->id;
964 $feedback->feedbacktext = $quiz->feedbacktext[$i];
965 $feedback->mingrade = $quiz->feedbackboundaries[$i];
966 $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
c18269c7 967 if (!$DB->insert_record('quiz_feedback', $feedback, false)) {
212b7b8c 968 return "Could not save quiz feedback.";
969 }
970 }
971
920b93d1 972 // Update the events relating to this quiz.
973 // This is slightly inefficient, deleting the old events and creating new ones. However,
974 // there are at most two events, and this keeps the code simpler.
c18269c7 975 if ($events = $DB->get_records('event', array('modulename'=>'quiz', 'instance'=>$quiz->id))) {
920b93d1 976 foreach($events as $event) {
977 delete_event($event->id);
978 }
979 }
980
981 $event = new stdClass;
982 $event->description = $quiz->intro;
983 $event->courseid = $quiz->course;
984 $event->groupid = 0;
985 $event->userid = 0;
986 $event->modulename = 'quiz';
987 $event->instance = $quiz->id;
988 $event->timestart = $quiz->timeopen;
989 $event->timeduration = $quiz->timeclose - $quiz->timeopen;
990 $event->visible = instance_is_visible('quiz', $quiz);
991 $event->eventtype = 'open';
992
993 if ($quiz->timeclose and $quiz->timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) {
994 // Single event for the whole quiz.
995 $event->name = $quiz->name;
996 add_event($event);
997 } else {
998 // Separate start and end events.
999 $event->timeduration = 0;
1000 if ($quiz->timeopen) {
1001 $event->name = $quiz->name.' ('.get_string('quizopens', 'quiz').')';
1002 add_event($event);
1003 unset($event->id); // So we can use the same object for the close event.
1004 }
1005 if ($quiz->timeclose) {
1006 $event->name = $quiz->name.' ('.get_string('quizcloses', 'quiz').')';
1007 $event->timestart = $quiz->timeclose;
1008 $event->eventtype = 'close';
1009 add_event($event);
1010 }
1011 }
d6dd2108 1012
1013 //update related grade item
c18269c7 1014 quiz_grade_item_update($quiz);
ee1fb969 1015}
1016
f3221af9 1017function quiz_get_view_actions() {
acf149ad 1018 return array('view', 'view all', 'report', 'review');
f3221af9 1019}
ee1fb969 1020
f3221af9 1021function quiz_get_post_actions() {
acf149ad 1022 return array('attempt', 'close attempt', 'preview', 'editquestions', 'delete attempt', 'manualgrade');
f3221af9 1023}
ee1fb969 1024
f67172b6 1025/**
1026 * Returns an array of names of quizzes that use this question
1027 *
f67172b6 1028 * @param object $questionid
1029 * @return array of strings
1030 */
1031function quiz_question_list_instances($questionid) {
c6307ef2 1032 global $CFG, $DB;
e8666d9a 1033
1034 // TODO: we should also consider other questions that are used by
1035 // random questions in this quiz, but that is very hard.
1036
1037 $sql = "SELECT q.id, q.name
c6307ef2 1038 FROM {quiz} q
1039 JOIN {quiz_question_instances} qqi ON q.id = qqi.quiz
1040 WHERE qqi.question = ?";
e8666d9a 1041
c6307ef2 1042 if ($instances = $DB->get_records_sql_menu($sql, array($questionid))) {
e8666d9a 1043 return $instances;
1044 }
f67172b6 1045 return array();
1046}
1047
7a6f4066 1048/**
1049 * Implementation of the function for printing the form elements that control
1050 * whether the course reset functionality affects the quiz.
0b5a80a1 1051 * @param $mform form passed by reference
1052 */
1053function quiz_reset_course_form_definition(&$mform) {
c159da4c 1054 $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz'));
0b5a80a1 1055 $mform->addElement('advcheckbox', 'reset_quiz_attempts', get_string('removeallquizattempts','quiz'));
1056}
1057
1058/**
1059 * Course reset form defaults.
1060 */
1061function quiz_reset_course_form_defaults($course) {
1062 return array('reset_quiz_attempts'=>1);
1063}
1064
1065/**
1066 * Removes all grades from gradebook
1067 * @param int $courseid
1068 * @param string optional type
7a6f4066 1069 */
0b5a80a1 1070function quiz_reset_gradebook($courseid, $type='') {
9cf4a18b 1071 global $CFG, $DB;
0b5a80a1 1072
1073 $sql = "SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
9cf4a18b 1074 FROM {quiz} q, {course_modules} cm, {modules} m
1075 WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=q.id AND q.course=?";
0b5a80a1 1076
9cf4a18b 1077 if ($quizs = $DB->get_records_sql($sql, array($courseid))) {
0b5a80a1 1078 foreach ($quizs as $quiz) {
1079 quiz_grade_item_update($quiz, 'reset');
1080 }
1081 }
7a6f4066 1082}
1083
1084/**
1085 * Actual implementation of the rest coures functionality, delete all the
1086 * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is
1087 * set and true.
6ef56c99 1088 *
1089 * Also, move the quiz open and close dates, if the course start date is changing.
0b5a80a1 1090 * @param $data the data submitted from the reset course.
1091 * @return array status array
7a6f4066 1092 */
0b5a80a1 1093function quiz_reset_userdata($data) {
53004e48 1094 global $CFG, $DB;
1095 require_once($CFG->libdir.'/questionlib.php');
be0ba083 1096
0b5a80a1 1097 $componentstr = get_string('modulenameplural', 'quiz');
1098 $status = array();
b159da78 1099
6ef56c99 1100 /// Delete attempts.
1101 if (!empty($data->reset_quiz_attempts)) {
53004e48 1102 $quizzes = $DB->get_records('quiz', array('course' => $data->courseid));
1103 foreach ($quizzes as $quiz) {
1104 quiz_delete_all_attempts($quiz);
7a6f4066 1105 }
0b5a80a1 1106
0b5a80a1 1107 // remove all grades from gradebook
1108 if (empty($data->reset_gradebook_grades)) {
1109 quiz_reset_gradebook($data->courseid);
7a6f4066 1110 }
53004e48 1111 $status[] = array('component' => $componentstr, 'item' => get_string('attemptsdeleted', 'quiz'), 'error' => false);
7a6f4066 1112 }
6ef56c99 1113
0b5a80a1 1114 /// updating dates - shift may be negative too
1115 if ($data->timeshift) {
1116 shift_course_mod_dates('quiz', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
53004e48 1117 $status[] = array('component' => $componentstr, 'item' => get_string('openclosedatesupdated', 'quiz'), 'error' => false);
7a6f4066 1118 }
0b5a80a1 1119
1120 return $status;
7a6f4066 1121}
14e6dc79 1122
1123/**
1124 * Checks whether the current user is allowed to view a file uploaded in a quiz.
1125 * Teachers can view any from their courses, students can only view their own.
b159da78 1126 *
95de57b8 1127 * @param int $attemptuniqueid int attempt id
14e6dc79 1128 * @param int $questionid int question id
b159da78 1129 * @return boolean to indicate access granted or denied
14e6dc79 1130 */
95de57b8 1131function quiz_check_file_access($attemptuniqueid, $questionid) {
9cf4a18b 1132 global $USER, $DB;
b159da78 1133
6102a59d 1134 $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $attemptuniqueid));
9cf4a18b 1135 $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz));
14e6dc79 1136 $context = get_context_instance(CONTEXT_COURSE, $quiz->course);
b159da78 1137
14e6dc79 1138 // access granted if the current user submitted this file
1139 if ($attempt->userid == $USER->id) {
1140 return true;
1141 // access granted if the current user has permission to grade quizzes in this course
1142 } else if (has_capability('mod/quiz:viewreports', $context) || has_capability('mod/quiz:grade', $context)) {
1143 return true;
1144 }
b159da78 1145
1146 // otherwise, this user does not have permission
14e6dc79 1147 return false;
1148}
b5a16eb7 1149
1150/**
1151 * Prints quiz summaries on MyMoodle Page
1152 */
1153function quiz_print_overview($courses, &$htmlarray) {
1154 global $USER, $CFG;
1155/// These next 6 Lines are constant in all modules (just change module name)
1156 if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1157 return array();
1158 }
1159
2a13e454 1160 if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
b5a16eb7 1161 return;
1162 }
1163
1164/// Fetch some language strings outside the main loop.
1165 $strquiz = get_string('modulename', 'quiz');
1166 $strnoattempts = get_string('noattempts', 'quiz');
1167
1168/// We want to list quizzes that are currently available, and which have a close date.
1169/// This is the same as what the lesson does, and the dabate is in MDL-10568.
6c58e198 1170 $now = time();
2a13e454 1171 foreach ($quizzes as $quiz) {
b5a16eb7 1172 if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
1173 /// Give a link to the quiz, and the deadline.
1174 $str = '<div class="quiz overview">' .
1175 '<div class="name">' . $strquiz . ': <a ' . ($quiz->visible ? '' : ' class="dimmed"') .
1176 ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->coursemodule . '">' .
1177 $quiz->name . '</a></div>';
1178 $str .= '<div class="info">' . get_string('quizcloseson', 'quiz', userdate($quiz->timeclose)) . '</div>';
1179
1180 /// Now provide more information depending on the uers's role.
1181 $context = get_context_instance(CONTEXT_MODULE, $quiz->coursemodule);
1182 if (has_capability('mod/quiz:viewreports', $context)) {
1183 /// For teacher-like people, show a summary of the number of student attempts.
9cf4a18b 1184 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
2a13e454 1185 // fields set to make the following call work.
7956944f 1186 $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
96c7d771 1187 } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student
b5a16eb7 1188 /// For student-like people, tell them how many attempts they have made.
1189 if (isset($USER->id) && ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
1190 $numattempts = count($attempts);
9cf4a18b 1191 $str .= '<div class="info">' . get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
b5a16eb7 1192 } else {
1193 $str .= '<div class="info">' . $strnoattempts . '</div>';
1194 }
1195 } else {
1196 /// For ayone else, there is no point listing this quiz, so stop processing.
1197 continue;
1198 }
1199
1200 /// Add the output for this quiz to the rest.
1201 $str .= '</div>';
1202 if (empty($htmlarray[$quiz->course]['quiz'])) {
1203 $htmlarray[$quiz->course]['quiz'] = $str;
1204 } else {
1205 $htmlarray[$quiz->course]['quiz'] .= $str;
1206 }
1207 }
1208 }
1209}
6c58e198 1210
1211/**
1212 * Return a textual summary of the number of attemtps that have been made at a particular quiz,
1213 * returns '' if no attemtps have been made yet, unless $returnzero is passed as true.
1214 * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
2a13e454 1215 * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
1216 * @param boolean $returnzero if false (default), when no attempts have been made '' is returned instead of 'Attempts: 0'.
1217 * @param int $currentgroup if there is a concept of current group where this method is being called
1218 * (e.g. a report) pass it in here. Default 0 which means no current group.
1219 * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
1220 * "Attemtps 123 (45 from this group)".
6c58e198 1221 */
2a13e454 1222function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
9cf4a18b 1223 global $CFG, $USER, $DB;
1224 $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0));
6c58e198 1225 if ($numattempts || $returnzero) {
2a13e454 1226 if (groups_get_activity_groupmode($cm)) {
1227 $a->total = $numattempts;
1228 if ($currentgroup) {
9cf4a18b 1229 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1230 '{quiz_attempts} qa JOIN ' .
1231 '{groups_members} gm ON qa.userid = gm.userid ' .
1232 'WHERE quiz = ? AND preview = 0 AND groupid = ?', array($quiz->id, $currentgroup));
2a13e454 1233 return get_string('attemptsnumthisgroup', 'quiz', $a);
9cf4a18b 1234 } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
1235 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups));
1236 $a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
1237 '{quiz_attempts} qa JOIN ' .
1238 '{groups_members} gm ON qa.userid = gm.userid ' .
1239 'WHERE quiz = ? AND preview = 0 AND ' .
1240 "groupid $usql", array_merge(array($quiz->id), $params));
2a13e454 1241 return get_string('attemptsnumyourgroups', 'quiz', $a);
1242 }
1243 }
6c58e198 1244 return get_string('attemptsnum', 'quiz', $numattempts);
1245 }
1246 return '';
1247}
f432bebf 1248
4e781c7b 1249/**
1250 * @param string $feature FEATURE_xx constant for requested feature
1251 * @return bool True if quiz supports feature
1252 */
1253function quiz_supports($feature) {
1254 switch($feature) {
42f103be 1255 case FEATURE_GROUPS: return true;
1256 case FEATURE_GROUPINGS: return true;
1257 case FEATURE_GROUPMEMBERSONLY: return true;
1258 case FEATURE_MODEDIT_INTRO_EDITOR: return true;
4e781c7b 1259 case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
42f103be 1260 case FEATURE_GRADE_HAS_GRADE: return true;
1261 case FEATURE_GRADE_OUTCOMES: return true;
1262
49f6e5f4 1263 default: return null;
4e781c7b 1264 }
1265}
1266
f432bebf 1267/**
cca6e300 1268 * @return array all other caps used in module
f432bebf 1269 */
1270function quiz_get_extra_capabilities() {
be0ba083 1271 global $DB, $CFG;
1272 require_once($CFG->libdir.'/questionlib.php');
cca6e300 1273 $caps = question_get_all_capabilities();
bbf4f440 1274 $reportcaps = $DB->get_records_select_menu('capabilities', 'name LIKE ?', array('quizreport/%'), 'id,name');
1275 $caps = array_merge($caps, $reportcaps);
cca6e300 1276 $caps[] = 'moodle/site:accessallgroups';
1277 return $caps;
f432bebf 1278}
1279