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