Commit | Line | Data |
---|---|---|
83192608 | 1 | <?php |
25302dee TH |
2 | // This file is part of Moodle - http://moodle.org/ |
3 | // | |
4 | // Moodle is free software: you can redistribute it and/or modify | |
5 | // it under the terms of the GNU General Public License as published by | |
6 | // the Free Software Foundation, either version 3 of the License, or | |
7 | // (at your option) any later version. | |
8 | // | |
9 | // Moodle is distributed in the hope that it will be useful, | |
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | // GNU General Public License for more details. | |
13 | // | |
14 | // You should have received a copy of the GNU General Public License | |
15 | // along with Moodle. If not, see <http://www.gnu.org/licenses/>. | |
84e628a0 | 16 | |
ee1fb969 | 17 | /** |
26d12a0c | 18 | * Library of functions used by the quiz module. |
19 | * | |
20 | * This contains functions that are called from within the quiz module only | |
21 | * Functions that are also called by core Moodle are in {@link lib.php} | |
22 | * This script also loads the code in {@link questionlib.php} which holds | |
23 | * the module-indpendent code for handling questions and which in turn | |
24 | * initialises all the questiontype classes. | |
f63a4ff2 | 25 | * |
01773a6d | 26 | * @package mod_quiz |
ba643847 TH |
27 | * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} |
28 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
26d12a0c | 29 | */ |
76cacec8 | 30 | |
66d07f81 | 31 | |
a17b297d TH |
32 | defined('MOODLE_INTERNAL') || die(); |
33 | ||
26d12a0c | 34 | require_once($CFG->dirroot . '/mod/quiz/lib.php'); |
ea38245c | 35 | require_once($CFG->dirroot . '/mod/quiz/accessmanager.php'); |
987c2d49 | 36 | require_once($CFG->dirroot . '/mod/quiz/accessmanager_form.php'); |
39759ac4 | 37 | require_once($CFG->dirroot . '/mod/quiz/renderer.php'); |
78e7a3dd | 38 | require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); |
beb992af JPG |
39 | require_once($CFG->libdir . '/completionlib.php'); |
40 | require_once($CFG->libdir . '/eventslib.php'); | |
99d19c13 | 41 | require_once($CFG->libdir . '/filelib.php'); |
16fa272e | 42 | require_once($CFG->libdir . '/questionlib.php'); |
76cacec8 | 43 | |
76cacec8 | 44 | |
c52c62d1 | 45 | /** |
9e83f3d1 | 46 | * @var int We show the countdown timer if there is less than this amount of time left before the |
c52c62d1 | 47 | * the quiz close date. (1 hour) |
48 | */ | |
49 | define('QUIZ_SHOW_TIME_BEFORE_DEADLINE', '3600'); | |
50 | ||
34b7d838 TH |
51 | /** |
52 | * @var int If there are fewer than this many seconds left when the student submits | |
53 | * a page of the quiz, then do not take them to the next page of the quiz. Instead | |
54 | * close the quiz immediately. | |
55 | */ | |
56 | define('QUIZ_MIN_TIME_TO_CONTINUE', '2'); | |
57 | ||
1ecd3c30 JA |
58 | /** |
59 | * @var int We show no image when user selects No image from dropdown menu in quiz settings. | |
60 | */ | |
61 | define('QUIZ_SHOWIMAGE_NONE', 0); | |
62 | ||
63 | /** | |
64 | * @var int We show small image when user selects small image from dropdown menu in quiz settings. | |
65 | */ | |
66 | define('QUIZ_SHOWIMAGE_SMALL', 1); | |
67 | ||
68 | /** | |
69 | * @var int We show Large image when user selects Large image from dropdown menu in quiz settings. | |
70 | */ | |
71 | define('QUIZ_SHOWIMAGE_LARGE', 2); | |
72 | ||
34b7d838 | 73 | |
9e83f3d1 | 74 | // Functions related to attempts /////////////////////////////////////////////// |
ee1fb969 | 75 | |
76 | /** | |
26d12a0c | 77 | * Creates an object to represent a new attempt at a quiz |
78 | * | |
79 | * Creates an attempt object to represent an attempt at the quiz by the current | |
80 | * user starting at the current time. The ->id field is not set. The object is | |
81 | * NOT written to the database. | |
c52c62d1 | 82 | * |
8e771aed | 83 | * @param object $quizobj the quiz object to create an attempt for. |
f7970e3c | 84 | * @param int $attemptnumber the sequence number for the attempt. |
c52c62d1 | 85 | * @param object $lastattempt the previous attempt by this user, if any. Only needed |
86 | * if $attemptnumber > 1 and $quiz->attemptonlast is true. | |
f7970e3c TH |
87 | * @param int $timenow the time the attempt was started at. |
88 | * @param bool $ispreview whether this new attempt is a preview. | |
70782526 | 89 | * @param int $userid the id of the user attempting this quiz. |
c52c62d1 | 90 | * |
91 | * @return object the newly created attempt object. | |
26d12a0c | 92 | */ |
70782526 | 93 | function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timenow, $ispreview = false, $userid = null) { |
c52c62d1 | 94 | global $USER; |
ee1fb969 | 95 | |
70782526 JP |
96 | if ($userid === null) { |
97 | $userid = $USER->id; | |
98 | } | |
99 | ||
8e771aed | 100 | $quiz = $quizobj->get_quiz(); |
18dff757 TH |
101 | if ($quiz->sumgrades < 0.000005 && $quiz->grade > 0.000005) { |
102 | throw new moodle_exception('cannotstartgradesmismatch', 'quiz', | |
ba3fedfb TH |
103 | new moodle_url('/mod/quiz/view.php', array('q' => $quiz->id)), |
104 | array('grade' => quiz_format_grade($quiz, $quiz->grade))); | |
18dff757 TH |
105 | } |
106 | ||
e89e09d2 | 107 | if ($attemptnumber == 1 || !$quiz->attemptonlast) { |
25a03faa | 108 | // We are not building on last attempt so create a new attempt. |
0ff4bd08 | 109 | $attempt = new stdClass(); |
75e1df6f | 110 | $attempt->quiz = $quiz->id; |
70782526 | 111 | $attempt->userid = $userid; |
75e1df6f | 112 | $attempt->preview = 0; |
ccba5b88 | 113 | $attempt->layout = ''; |
c52c62d1 | 114 | } else { |
25a03faa | 115 | // Build on last attempt. |
c52c62d1 | 116 | if (empty($lastattempt)) { |
5a2a5331 | 117 | print_error('cannotfindprevattempt', 'quiz'); |
c52c62d1 | 118 | } |
119 | $attempt = $lastattempt; | |
75e1df6f | 120 | } |
121 | ||
ee1fb969 | 122 | $attempt->attempt = $attemptnumber; |
ee1fb969 | 123 | $attempt->timestart = $timenow; |
124 | $attempt->timefinish = 0; | |
125 | $attempt->timemodified = $timenow; | |
8a972ab3 | 126 | $attempt->timemodifiedoffline = 0; |
be18f589 | 127 | $attempt->state = quiz_attempt::IN_PROGRESS; |
cd5be9a5 MG |
128 | $attempt->currentpage = 0; |
129 | $attempt->sumgrades = null; | |
75e1df6f | 130 | |
25a03faa | 131 | // If this is a preview, mark it as such. |
c52c62d1 | 132 | if ($ispreview) { |
133 | $attempt->preview = 1; | |
134 | } | |
135 | ||
8e771aed MP |
136 | $timeclose = $quizobj->get_access_manager($timenow)->get_end_time($attempt); |
137 | if ($timeclose === false || $ispreview) { | |
138 | $attempt->timecheckstate = null; | |
139 | } else { | |
140 | $attempt->timecheckstate = $timeclose; | |
141 | } | |
142 | ||
ee1fb969 | 143 | return $attempt; |
144 | } | |
1cd7c92e JP |
145 | /** |
146 | * Start a normal, new, quiz attempt. | |
147 | * | |
c2b051c7 JP |
148 | * @param quiz $quizobj the quiz object to start an attempt for. |
149 | * @param question_usage_by_activity $quba | |
150 | * @param object $attempt | |
151 | * @param integer $attemptnumber starting from 1 | |
152 | * @param integer $timenow the attempt start time | |
153 | * @param array $questionids slot number => question id. Used for random questions, to force the choice | |
154 | * of a particular actual question. Intended for testing purposes only. | |
155 | * @param array $forcedvariantsbyslot slot number => variant. Used for questions with variants, | |
156 | * to force the choice of a particular variant. Intended for testing | |
157 | * purposes only. | |
c2f2e7f0 | 158 | * @throws moodle_exception |
c2b051c7 | 159 | * @return object modified attempt object |
1cd7c92e | 160 | */ |
c2b051c7 JP |
161 | function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, |
162 | $questionids = array(), $forcedvariantsbyslot = array()) { | |
bb93fc24 TH |
163 | |
164 | // Usages for this user's previous quiz attempts. | |
165 | $qubaids = new \mod_quiz\question\qubaids_for_users_attempts( | |
166 | $quizobj->get_quizid(), $attempt->userid); | |
167 | ||
1cd7c92e JP |
168 | // Fully load all the questions in this quiz. |
169 | $quizobj->preload_questions(); | |
170 | $quizobj->load_questions(); | |
171 | ||
bb93fc24 TH |
172 | // First load all the non-random questions. |
173 | $randomfound = false; | |
174 | $slot = 0; | |
175 | $questions = array(); | |
176 | $maxmark = array(); | |
5d949702 | 177 | $page = array(); |
ccba5b88 | 178 | foreach ($quizobj->get_questions() as $questiondata) { |
bb93fc24 TH |
179 | $slot += 1; |
180 | $maxmark[$slot] = $questiondata->maxmark; | |
5d949702 | 181 | $page[$slot] = $questiondata->page; |
bb93fc24 TH |
182 | if ($questiondata->qtype == 'random') { |
183 | $randomfound = true; | |
184 | continue; | |
185 | } | |
186 | if (!$quizobj->get_quiz()->shuffleanswers) { | |
187 | $questiondata->options->shuffleanswers = false; | |
188 | } | |
189 | $questions[$slot] = question_bank::make_question($questiondata); | |
190 | } | |
1cd7c92e | 191 | |
bb93fc24 TH |
192 | // Then find a question to go in place of each random question. |
193 | if ($randomfound) { | |
194 | $slot = 0; | |
195 | $usedquestionids = array(); | |
196 | foreach ($questions as $question) { | |
197 | if (isset($usedquestions[$question->id])) { | |
198 | $usedquestionids[$question->id] += 1; | |
c2f2e7f0 | 199 | } else { |
bb93fc24 TH |
200 | $usedquestionids[$question->id] = 1; |
201 | } | |
202 | } | |
203 | $randomloader = new \core_question\bank\random_question_loader($qubaids, $usedquestionids); | |
204 | ||
205 | foreach ($quizobj->get_questions() as $questiondata) { | |
206 | $slot += 1; | |
207 | if ($questiondata->qtype != 'random') { | |
208 | continue; | |
209 | } | |
210 | ||
211 | // Deal with fixed random choices for testing. | |
212 | if (isset($questionids[$quba->next_slot_number()])) { | |
213 | if ($randomloader->is_question_available($questiondata->category, | |
214 | (bool) $questiondata->questiontext, $questionids[$quba->next_slot_number()])) { | |
215 | $questions[$slot] = question_bank::load_question( | |
216 | $questionids[$quba->next_slot_number()], $quizobj->get_quiz()->shuffleanswers); | |
217 | continue; | |
218 | } else { | |
219 | throw new coding_exception('Forced question id not available.'); | |
220 | } | |
c2f2e7f0 JP |
221 | } |
222 | ||
bb93fc24 TH |
223 | // Normal case, pick one at random. |
224 | $questionid = $randomloader->get_next_question_id($questiondata->category, | |
225 | (bool) $questiondata->questiontext); | |
226 | if ($questionid === null) { | |
1cd7c92e JP |
227 | throw new moodle_exception('notenoughrandomquestions', 'quiz', |
228 | $quizobj->view_url(), $questiondata); | |
229 | } | |
bb93fc24 TH |
230 | |
231 | $questions[$slot] = question_bank::load_question($questionid, | |
232 | $quizobj->get_quiz()->shuffleanswers); | |
1cd7c92e | 233 | } |
bb93fc24 | 234 | } |
1cd7c92e | 235 | |
bb93fc24 TH |
236 | // Finally add them all to the usage. |
237 | ksort($questions); | |
238 | foreach ($questions as $slot => $question) { | |
239 | $newslot = $quba->add_question($question, $maxmark[$slot]); | |
240 | if ($newslot != $slot) { | |
241 | throw new coding_exception('Slot numbers have got confused.'); | |
242 | } | |
1cd7c92e JP |
243 | } |
244 | ||
245 | // Start all the questions. | |
bb93fc24 | 246 | $variantstrategy = new core_question\engine\variants\least_used_strategy($quba, $qubaids); |
c2b051c7 JP |
247 | |
248 | if (!empty($forcedvariantsbyslot)) { | |
249 | $forcedvariantsbyseed = question_variant_forced_choices_selection_strategy::prepare_forced_choices_array( | |
250 | $forcedvariantsbyslot, $quba); | |
251 | $variantstrategy = new question_variant_forced_choices_selection_strategy( | |
252 | $forcedvariantsbyseed, $variantstrategy); | |
253 | } | |
254 | ||
255 | $quba->start_all_questions($variantstrategy, $timenow); | |
1cd7c92e | 256 | |
ccba5b88 | 257 | // Work out the attempt layout. |
5d949702 K |
258 | $sections = $quizobj->get_sections(); |
259 | foreach ($sections as $i => $section) { | |
260 | if (isset($sections[$i + 1])) { | |
261 | $sections[$i]->lastslot = $sections[$i + 1]->firstslot - 1; | |
262 | } else { | |
263 | $sections[$i]->lastslot = count($questions); | |
264 | } | |
265 | } | |
266 | ||
ccba5b88 | 267 | $layout = array(); |
5d949702 K |
268 | foreach ($sections as $section) { |
269 | if ($section->shufflequestions) { | |
270 | $questionsinthissection = array(); | |
271 | for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) { | |
272 | $questionsinthissection[] = $slot; | |
273 | } | |
274 | shuffle($questionsinthissection); | |
275 | $questionsonthispage = 0; | |
276 | foreach ($questionsinthissection as $slot) { | |
277 | if ($questionsonthispage && $questionsonthispage == $quizobj->get_quiz()->questionsperpage) { | |
278 | $layout[] = 0; | |
279 | $questionsonthispage = 0; | |
280 | } | |
281 | $layout[] = $slot; | |
282 | $questionsonthispage += 1; | |
ccba5b88 | 283 | } |
ccba5b88 | 284 | |
5d949702 K |
285 | } else { |
286 | $currentpage = $page[$section->firstslot]; | |
287 | for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) { | |
288 | if ($currentpage !== null && $page[$slot] != $currentpage) { | |
289 | $layout[] = 0; | |
290 | } | |
291 | $layout[] = $slot; | |
292 | $currentpage = $page[$slot]; | |
ccba5b88 | 293 | } |
1cd7c92e | 294 | } |
ccba5b88 | 295 | |
5d949702 K |
296 | // Each section ends with a page break. |
297 | $layout[] = 0; | |
298 | } | |
ccba5b88 TH |
299 | $attempt->layout = implode(',', $layout); |
300 | ||
1cd7c92e JP |
301 | return $attempt; |
302 | } | |
303 | ||
304 | /** | |
305 | * Start a subsequent new attempt, in each attempt builds on last mode. | |
306 | * | |
307 | * @param question_usage_by_activity $quba this question usage | |
308 | * @param object $attempt this attempt | |
309 | * @param object $lastattempt last attempt | |
310 | * @return object modified attempt object | |
311 | * | |
312 | */ | |
313 | function quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt) { | |
314 | $oldquba = question_engine::load_questions_usage_by_activity($lastattempt->uniqueid); | |
315 | ||
316 | $oldnumberstonew = array(); | |
317 | foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) { | |
318 | $newslot = $quba->add_question($oldqa->get_question(), $oldqa->get_max_mark()); | |
319 | ||
320 | $quba->start_question_based_on($newslot, $oldqa); | |
321 | ||
322 | $oldnumberstonew[$oldslot] = $newslot; | |
323 | } | |
324 | ||
325 | // Update attempt layout. | |
326 | $newlayout = array(); | |
327 | foreach (explode(',', $lastattempt->layout) as $oldslot) { | |
328 | if ($oldslot != 0) { | |
329 | $newlayout[] = $oldnumberstonew[$oldslot]; | |
330 | } else { | |
331 | $newlayout[] = 0; | |
332 | } | |
333 | } | |
334 | $attempt->layout = implode(',', $newlayout); | |
335 | return $attempt; | |
336 | } | |
337 | ||
338 | /** | |
339 | * The save started question usage and quiz attempt in db and log the started attempt. | |
340 | * | |
341 | * @param quiz $quizobj | |
342 | * @param question_usage_by_activity $quba | |
343 | * @param object $attempt | |
344 | * @return object attempt object with uniqueid and id set. | |
345 | */ | |
346 | function quiz_attempt_save_started($quizobj, $quba, $attempt) { | |
347 | global $DB; | |
348 | // Save the attempt in the database. | |
349 | question_engine::save_questions_usage_by_activity($quba); | |
350 | $attempt->uniqueid = $quba->get_id(); | |
351 | $attempt->id = $DB->insert_record('quiz_attempts', $attempt); | |
a2caf56d MN |
352 | |
353 | // Params used by the events below. | |
354 | $params = array( | |
355 | 'objectid' => $attempt->id, | |
356 | 'relateduserid' => $attempt->userid, | |
357 | 'courseid' => $quizobj->get_courseid(), | |
358 | 'context' => $quizobj->get_context() | |
359 | ); | |
360 | // Decide which event we are using. | |
1cd7c92e | 361 | if ($attempt->preview) { |
a2caf56d MN |
362 | $params['other'] = array( |
363 | 'quizid' => $quizobj->get_quizid() | |
45e1e3bd MN |
364 | ); |
365 | $event = \mod_quiz\event\attempt_preview_started::create($params); | |
1cd7c92e | 366 | } else { |
a2caf56d MN |
367 | $event = \mod_quiz\event\attempt_started::create($params); |
368 | ||
1cd7c92e | 369 | } |
a2caf56d MN |
370 | |
371 | // Trigger the event. | |
372 | $event->add_record_snapshot('quiz', $quizobj->get_quiz()); | |
546a2c95 | 373 | $event->add_record_snapshot('quiz_attempts', $attempt); |
a2caf56d MN |
374 | $event->trigger(); |
375 | ||
1cd7c92e JP |
376 | return $attempt; |
377 | } | |
378 | ||
cd06115f | 379 | /** |
25302dee TH |
380 | * Returns an unfinished attempt (if there is one) for the given |
381 | * user on the given quiz. This function does not return preview attempts. | |
77ed3ba5 | 382 | * |
f7970e3c TH |
383 | * @param int $quizid the id of the quiz. |
384 | * @param int $userid the id of the user. | |
77ed3ba5 | 385 | * |
386 | * @return mixed the unfinished attempt if there is one, false if not. | |
cd06115f | 387 | */ |
0d156caa | 388 | function quiz_get_user_attempt_unfinished($quizid, $userid) { |
ed8bebce | 389 | $attempts = quiz_get_user_attempts($quizid, $userid, 'unfinished', true); |
77ed3ba5 | 390 | if ($attempts) { |
391 | return array_shift($attempts); | |
392 | } else { | |
393 | return false; | |
394 | } | |
920fb237 | 395 | } |
396 | ||
ff51d646 | 397 | /** |
398 | * Delete a quiz attempt. | |
25a03faa TH |
399 | * @param mixed $attempt an integer attempt id or an attempt object |
400 | * (row of the quiz_attempts table). | |
97344ae3 | 401 | * @param object $quiz the quiz object. |
ff51d646 | 402 | */ |
403 | function quiz_delete_attempt($attempt, $quiz) { | |
9cf4a18b | 404 | global $DB; |
ff51d646 | 405 | if (is_numeric($attempt)) { |
9cf4a18b | 406 | if (!$attempt = $DB->get_record('quiz_attempts', array('id' => $attempt))) { |
ff51d646 | 407 | return; |
408 | } | |
409 | } | |
271e6dec | 410 | |
ff51d646 | 411 | if ($attempt->quiz != $quiz->id) { |
412 | debugging("Trying to delete attempt $attempt->id which belongs to quiz $attempt->quiz " . | |
413 | "but was passed quiz $quiz->id."); | |
414 | return; | |
415 | } | |
271e6dec | 416 | |
a97a7585 MN |
417 | if (!isset($quiz->cmid)) { |
418 | $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course); | |
419 | $quiz->cmid = $cm->id; | |
420 | } | |
421 | ||
25302dee | 422 | question_engine::delete_questions_usage_by_activity($attempt->uniqueid); |
6b5f24d3 | 423 | $DB->delete_records('quiz_attempts', array('id' => $attempt->id)); |
ff51d646 | 424 | |
52940528 DP |
425 | // Log the deletion of the attempt if not a preview. |
426 | if (!$attempt->preview) { | |
427 | $params = array( | |
428 | 'objectid' => $attempt->id, | |
429 | 'relateduserid' => $attempt->userid, | |
430 | 'context' => context_module::instance($quiz->cmid), | |
431 | 'other' => array( | |
432 | 'quizid' => $quiz->id | |
433 | ) | |
434 | ); | |
435 | $event = \mod_quiz\event\attempt_deleted::create($params); | |
436 | $event->add_record_snapshot('quiz_attempts', $attempt); | |
437 | $event->trigger(); | |
438 | } | |
7dad3d95 | 439 | |
ff51d646 | 440 | // Search quiz_attempts for other instances by this user. |
441 | // If none, then delete record for this quiz, this user from quiz_grades | |
9e83f3d1 | 442 | // else recalculate best grade. |
ff51d646 | 443 | $userid = $attempt->userid; |
9cf4a18b | 444 | if (!$DB->record_exists('quiz_attempts', array('userid' => $userid, 'quiz' => $quiz->id))) { |
25a03faa | 445 | $DB->delete_records('quiz_grades', array('userid' => $userid, 'quiz' => $quiz->id)); |
ff51d646 | 446 | } else { |
447 | quiz_save_best_grade($quiz, $userid); | |
448 | } | |
07a7d859 | 449 | |
450 | quiz_update_grades($quiz, $userid); | |
ff51d646 | 451 | } |
452 | ||
4299df1d | 453 | /** |
454 | * Delete all the preview attempts at a quiz, or possibly all the attempts belonging | |
455 | * to one user. | |
456 | * @param object $quiz the quiz object. | |
f7970e3c | 457 | * @param int $userid (optional) if given, only delete the previews belonging to this user. |
4299df1d | 458 | */ |
459 | function quiz_delete_previews($quiz, $userid = null) { | |
460 | global $DB; | |
461 | $conditions = array('quiz' => $quiz->id, 'preview' => 1); | |
462 | if (!empty($userid)) { | |
463 | $conditions['userid'] = $userid; | |
464 | } | |
465 | $previewattempts = $DB->get_records('quiz_attempts', $conditions); | |
466 | foreach ($previewattempts as $attempt) { | |
467 | quiz_delete_attempt($attempt, $quiz); | |
468 | } | |
469 | } | |
470 | ||
84e628a0 | 471 | /** |
f7970e3c TH |
472 | * @param int $quizid The quiz id. |
473 | * @return bool whether this quiz has any (non-preview) attempts. | |
84e628a0 | 474 | */ |
475 | function quiz_has_attempts($quizid) { | |
476 | global $DB; | |
477 | return $DB->record_exists('quiz_attempts', array('quiz' => $quizid, 'preview' => 0)); | |
478 | } | |
479 | ||
9e83f3d1 | 480 | // Functions to do with quiz layout and pages ////////////////////////////////// |
0d156caa | 481 | |
ee1fb969 | 482 | /** |
ccba5b88 TH |
483 | * Repaginate the questions in a quiz |
484 | * @param int $quizid the id of the quiz to repaginate. | |
485 | * @param int $slotsperpage number of items to put on each page. 0 means unlimited. | |
26d12a0c | 486 | */ |
ccba5b88 TH |
487 | function quiz_repaginate_questions($quizid, $slotsperpage) { |
488 | global $DB; | |
489 | $trans = $DB->start_delegated_transaction(); | |
0c5abe6c | 490 | |
5d949702 K |
491 | $sections = $DB->get_records('quiz_sections', array('quizid' => $quizid), 'firstslot ASC'); |
492 | $firstslots = array(); | |
493 | foreach ($sections as $section) { | |
494 | if ((int)$section->firstslot === 1) { | |
495 | continue; | |
496 | } | |
497 | $firstslots[] = $section->firstslot; | |
498 | } | |
499 | ||
ccba5b88 TH |
500 | $slots = $DB->get_records('quiz_slots', array('quizid' => $quizid), |
501 | 'slot'); | |
ccba5b88 TH |
502 | $currentpage = 1; |
503 | $slotsonthispage = 0; | |
504 | foreach ($slots as $slot) { | |
5d949702 K |
505 | if (($firstslots && in_array($slot->slot, $firstslots)) || |
506 | ($slotsonthispage && $slotsonthispage == $slotsperpage)) { | |
ccba5b88 TH |
507 | $currentpage += 1; |
508 | $slotsonthispage = 0; | |
76cacec8 | 509 | } |
ccba5b88 TH |
510 | if ($slot->page != $currentpage) { |
511 | $DB->set_field('quiz_slots', 'page', $currentpage, array('id' => $slot->id)); | |
512 | } | |
513 | $slotsonthispage += 1; | |
76cacec8 | 514 | } |
0c5abe6c | 515 | |
ccba5b88 | 516 | $trans->allow_commit(); |
ee1fb969 | 517 | } |
76cacec8 | 518 | |
9e83f3d1 | 519 | // Functions to do with quiz grades //////////////////////////////////////////// |
76cacec8 | 520 | |
212b7b8c | 521 | /** |
522 | * Convert the raw grade stored in $attempt into a grade out of the maximum | |
523 | * grade for this quiz. | |
77ed3ba5 | 524 | * |
212b7b8c | 525 | * @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades |
25302dee | 526 | * @param object $quiz the quiz object. Only the fields grade, sumgrades and decimalpoints are used. |
f7970e3c | 527 | * @param bool|string $format whether to format the results for display |
25302dee | 528 | * or 'question' to format a question grade (different number of decimal places. |
25a03faa TH |
529 | * @return float|string the rescaled grade, or null/the lang string 'notyetgraded' |
530 | * if the $grade is null. | |
212b7b8c | 531 | */ |
25302dee TH |
532 | function quiz_rescale_grade($rawgrade, $quiz, $format = true) { |
533 | if (is_null($rawgrade)) { | |
534 | $grade = null; | |
535 | } else if ($quiz->sumgrades >= 0.000005) { | |
b7b4f8bd | 536 | $grade = $rawgrade * $quiz->grade / $quiz->sumgrades; |
212b7b8c | 537 | } else { |
b7b4f8bd | 538 | $grade = 0; |
212b7b8c | 539 | } |
25302dee TH |
540 | if ($format === 'question') { |
541 | $grade = quiz_format_question_grade($quiz, $grade); | |
542 | } else if ($format) { | |
543 | $grade = quiz_format_grade($quiz, $grade); | |
544 | } | |
b7b4f8bd | 545 | return $grade; |
76cacec8 | 546 | } |
547 | ||
48abca79 JL |
548 | /** |
549 | * Get the feedback object for this grade on this quiz. | |
550 | * | |
551 | * @param float $grade a grade on this quiz. | |
552 | * @param object $quiz the quiz settings. | |
553 | * @return false|stdClass the record object or false if there is not feedback for the given grade | |
554 | * @since Moodle 3.1 | |
555 | */ | |
556 | function quiz_feedback_record_for_grade($grade, $quiz) { | |
557 | global $DB; | |
558 | ||
559 | // With CBM etc, it is possible to get -ve grades, which would then not match | |
560 | // any feedback. Therefore, we replace -ve grades with 0. | |
561 | $grade = max($grade, 0); | |
562 | ||
563 | $feedback = $DB->get_record_select('quiz_feedback', | |
564 | 'quizid = ? AND mingrade <= ? AND ? < maxgrade', array($quiz->id, $grade, $grade)); | |
565 | ||
566 | return $feedback; | |
567 | } | |
568 | ||
ee1fb969 | 569 | /** |
212b7b8c | 570 | * Get the feedback text that should be show to a student who |
de266537 | 571 | * got this grade on this quiz. The feedback is processed ready for diplay. |
77ed3ba5 | 572 | * |
212b7b8c | 573 | * @param float $grade a grade on this quiz. |
2709ee45 TH |
574 | * @param object $quiz the quiz settings. |
575 | * @param object $context the quiz context. | |
212b7b8c | 576 | * @return string the comment that corresponds to this grade (empty string if there is not one. |
577 | */ | |
2709ee45 | 578 | function quiz_feedback_for_grade($grade, $quiz, $context) { |
212b7b8c | 579 | |
25302dee TH |
580 | if (is_null($grade)) { |
581 | return ''; | |
582 | } | |
583 | ||
48abca79 | 584 | $feedback = quiz_feedback_record_for_grade($grade, $quiz); |
fe6ce234 DC |
585 | |
586 | if (empty($feedback->feedbacktext)) { | |
2709ee45 | 587 | return ''; |
212b7b8c | 588 | } |
77ed3ba5 | 589 | |
de266537 | 590 | // Clean the text, ready for display. |
0ff4bd08 | 591 | $formatoptions = new stdClass(); |
de266537 | 592 | $formatoptions->noclean = true; |
25a03faa TH |
593 | $feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php', |
594 | $context->id, 'mod_quiz', 'feedback', $feedback->id); | |
fe6ce234 | 595 | $feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions); |
de266537 | 596 | |
fe6ce234 | 597 | return $feedbacktext; |
212b7b8c | 598 | } |
599 | ||
600 | /** | |
8ce3e17f | 601 | * @param object $quiz the quiz database row. |
f7970e3c | 602 | * @return bool Whether this quiz has any non-blank feedback text. |
212b7b8c | 603 | */ |
739b0711 | 604 | function quiz_has_feedback($quiz) { |
9cf4a18b | 605 | global $DB; |
212b7b8c | 606 | static $cache = array(); |
739b0711 | 607 | if (!array_key_exists($quiz->id, $cache)) { |
608 | $cache[$quiz->id] = quiz_has_grades($quiz) && | |
609 | $DB->record_exists_select('quiz_feedback', "quizid = ? AND " . | |
610 | $DB->sql_isnotempty('quiz_feedback', 'feedbacktext', false, true), | |
611 | array($quiz->id)); | |
212b7b8c | 612 | } |
739b0711 | 613 | return $cache[$quiz->id]; |
212b7b8c | 614 | } |
615 | ||
25302dee TH |
616 | /** |
617 | * Update the sumgrades field of the quiz. This needs to be called whenever | |
618 | * the grading structure of the quiz is changed. For example if a question is | |
619 | * added or removed, or a question weight is changed. | |
620 | * | |
18dff757 TH |
621 | * You should call {@link quiz_delete_previews()} before you call this function. |
622 | * | |
25302dee TH |
623 | * @param object $quiz a quiz. |
624 | */ | |
625 | function quiz_update_sumgrades($quiz) { | |
626 | global $DB; | |
18dff757 | 627 | |
25302dee TH |
628 | $sql = 'UPDATE {quiz} |
629 | SET sumgrades = COALESCE(( | |
dc4a3ea1 | 630 | SELECT SUM(maxmark) |
ccba5b88 | 631 | FROM {quiz_slots} |
dc4a3ea1 | 632 | WHERE quizid = {quiz}.id |
25302dee TH |
633 | ), 0) |
634 | WHERE id = ?'; | |
a13d4fbd TH |
635 | $DB->execute($sql, array($quiz->id)); |
636 | $quiz->sumgrades = $DB->get_field('quiz', 'sumgrades', array('id' => $quiz->id)); | |
18dff757 TH |
637 | |
638 | if ($quiz->sumgrades < 0.000005 && quiz_has_attempts($quiz->id)) { | |
639 | // If the quiz has been attempted, and the sumgrades has been | |
640 | // set to 0, then we must also set the maximum possible grade to 0, or | |
641 | // we will get a divide by zero error. | |
25302dee TH |
642 | quiz_set_grade(0, $quiz); |
643 | } | |
644 | } | |
645 | ||
18dff757 TH |
646 | /** |
647 | * Update the sumgrades field of the attempts at a quiz. | |
648 | * | |
649 | * @param object $quiz a quiz. | |
650 | */ | |
25302dee TH |
651 | function quiz_update_all_attempt_sumgrades($quiz) { |
652 | global $DB; | |
653 | $dm = new question_engine_data_mapper(); | |
654 | $timenow = time(); | |
655 | ||
656 | $sql = "UPDATE {quiz_attempts} | |
657 | SET | |
658 | timemodified = :timenow, | |
659 | sumgrades = ( | |
660 | {$dm->sum_usage_marks_subquery('uniqueid')} | |
661 | ) | |
be18f589 TH |
662 | WHERE quiz = :quizid AND state = :finishedstate"; |
663 | $DB->execute($sql, array('timenow' => $timenow, 'quizid' => $quiz->id, | |
664 | 'finishedstate' => quiz_attempt::FINISHED)); | |
25302dee TH |
665 | } |
666 | ||
212b7b8c | 667 | /** |
b2607ccc | 668 | * The quiz grade is the maximum that student's results are marked out of. When it |
212b7b8c | 669 | * changes, the corresponding data in quiz_grades and quiz_feedback needs to be |
25302dee TH |
670 | * rescaled. After calling this function, you probably need to call |
671 | * quiz_update_all_attempt_sumgrades, quiz_update_all_final_grades and | |
672 | * quiz_update_grades. | |
77ed3ba5 | 673 | * |
212b7b8c | 674 | * @param float $newgrade the new maximum grade for the quiz. |
25a03faa TH |
675 | * @param object $quiz the quiz we are updating. Passed by reference so its |
676 | * grade field can be updated too. | |
f7970e3c | 677 | * @return bool indicating success or failure. |
212b7b8c | 678 | */ |
25302dee | 679 | function quiz_set_grade($newgrade, $quiz) { |
9cf4a18b | 680 | global $DB; |
212b7b8c | 681 | // This is potentially expensive, so only do it if necessary. |
682 | if (abs($quiz->grade - $newgrade) < 1e-7) { | |
683 | // Nothing to do. | |
684 | return true; | |
685 | } | |
b22fc901 | 686 | |
62a3bfb3 TH |
687 | $oldgrade = $quiz->grade; |
688 | $quiz->grade = $newgrade; | |
689 | ||
212b7b8c | 690 | // Use a transaction, so that on those databases that support it, this is safer. |
d5a8d9aa PS |
691 | $transaction = $DB->start_delegated_transaction(); |
692 | ||
0f1c59c5 TH |
693 | // Update the quiz table. |
694 | $DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance)); | |
695 | ||
62a3bfb3 TH |
696 | if ($oldgrade < 1) { |
697 | // If the old grade was zero, we cannot rescale, we have to recompute. | |
698 | // We also recompute if the old grade was too small to avoid underflow problems. | |
699 | quiz_update_all_final_grades($quiz); | |
0f1c59c5 | 700 | |
62a3bfb3 TH |
701 | } else { |
702 | // We can rescale the grades efficiently. | |
0f1c59c5 TH |
703 | $timemodified = time(); |
704 | $DB->execute(" | |
705 | UPDATE {quiz_grades} | |
706 | SET grade = ? * grade, timemodified = ? | |
707 | WHERE quiz = ? | |
62a3bfb3 TH |
708 | ", array($newgrade/$oldgrade, $timemodified, $quiz->id)); |
709 | } | |
0f1c59c5 | 710 | |
62a3bfb3 | 711 | if ($oldgrade > 1e-7) { |
0f1c59c5 | 712 | // Update the quiz_feedback table. |
62a3bfb3 | 713 | $factor = $newgrade/$oldgrade; |
0f1c59c5 TH |
714 | $DB->execute(" |
715 | UPDATE {quiz_feedback} | |
716 | SET mingrade = ? * mingrade, maxgrade = ? * maxgrade | |
717 | WHERE quizid = ? | |
718 | ", array($factor, $factor, $quiz->id)); | |
719 | } | |
77ed3ba5 | 720 | |
9e83f3d1 | 721 | // Update grade item and send all grades to gradebook. |
0f1c59c5 TH |
722 | quiz_grade_item_update($quiz); |
723 | quiz_update_grades($quiz); | |
d6dd2108 | 724 | |
0f1c59c5 TH |
725 | $transaction->allow_commit(); |
726 | return true; | |
212b7b8c | 727 | } |
728 | ||
729 | /** | |
730 | * Save the overall grade for a user at a quiz in the quiz_grades table | |
731 | * | |
732 | * @param object $quiz The quiz for which the best grade is to be calculated and then saved. | |
f7970e3c | 733 | * @param int $userid The userid to calculate the grade for. Defaults to the current user. |
98f38217 | 734 | * @param array $attempts The attempts of this user. Useful if you are |
735 | * looping through many users. Attempts can be fetched in one master query to | |
736 | * avoid repeated querying. | |
f7970e3c | 737 | * @return bool Indicates success or failure. |
212b7b8c | 738 | */ |
98f38217 | 739 | function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) { |
cef18275 | 740 | global $DB, $OUTPUT, $USER; |
ee1fb969 | 741 | |
212b7b8c | 742 | if (empty($userid)) { |
ee1fb969 | 743 | $userid = $USER->id; |
744 | } | |
76cacec8 | 745 | |
25a03faa | 746 | if (!$attempts) { |
9e83f3d1 | 747 | // Get all the attempts made by the user. |
25302dee | 748 | $attempts = quiz_get_user_attempts($quiz->id, $userid); |
76cacec8 | 749 | } |
750 | ||
9e83f3d1 | 751 | // Calculate the best grade. |
76cacec8 | 752 | $bestgrade = quiz_calculate_best_grade($quiz, $attempts); |
84e628a0 | 753 | $bestgrade = quiz_rescale_grade($bestgrade, $quiz, false); |
77ed3ba5 | 754 | |
9e83f3d1 | 755 | // Save the best grade in the database. |
25302dee TH |
756 | if (is_null($bestgrade)) { |
757 | $DB->delete_records('quiz_grades', array('quiz' => $quiz->id, 'userid' => $userid)); | |
758 | ||
25a03faa TH |
759 | } else if ($grade = $DB->get_record('quiz_grades', |
760 | array('quiz' => $quiz->id, 'userid' => $userid))) { | |
fc44ee0d | 761 | $grade->grade = $bestgrade; |
76cacec8 | 762 | $grade->timemodified = time(); |
a8c31db2 | 763 | $DB->update_record('quiz_grades', $grade); |
25302dee | 764 | |
76cacec8 | 765 | } else { |
b640e681 | 766 | $grade = new stdClass(); |
76cacec8 | 767 | $grade->quiz = $quiz->id; |
768 | $grade->userid = $userid; | |
fc44ee0d | 769 | $grade->grade = $bestgrade; |
76cacec8 | 770 | $grade->timemodified = time(); |
a8c31db2 | 771 | $DB->insert_record('quiz_grades', $grade); |
76cacec8 | 772 | } |
d6dd2108 | 773 | |
774 | quiz_update_grades($quiz, $userid); | |
76cacec8 | 775 | } |
776 | ||
fc44ee0d | 777 | /** |
26d12a0c | 778 | * Calculate the overall grade for a quiz given a number of attempts by a particular user. |
779 | * | |
13bbfebc TH |
780 | * @param object $quiz the quiz settings object. |
781 | * @param array $attempts an array of all the user's attempts at this quiz in order. | |
782 | * @return float the overall grade | |
26d12a0c | 783 | */ |
76cacec8 | 784 | function quiz_calculate_best_grade($quiz, $attempts) { |
76cacec8 | 785 | |
786 | switch ($quiz->grademethod) { | |
787 | ||
ee1fb969 | 788 | case QUIZ_ATTEMPTFIRST: |
13bbfebc TH |
789 | $firstattempt = reset($attempts); |
790 | return $firstattempt->sumgrades; | |
76cacec8 | 791 | |
ee1fb969 | 792 | case QUIZ_ATTEMPTLAST: |
13bbfebc TH |
793 | $lastattempt = end($attempts); |
794 | return $lastattempt->sumgrades; | |
76cacec8 | 795 | |
ee1fb969 | 796 | case QUIZ_GRADEAVERAGE: |
76cacec8 | 797 | $sum = 0; |
798 | $count = 0; | |
799 | foreach ($attempts as $attempt) { | |
25302dee TH |
800 | if (!is_null($attempt->sumgrades)) { |
801 | $sum += $attempt->sumgrades; | |
802 | $count++; | |
803 | } | |
76cacec8 | 804 | } |
25302dee TH |
805 | if ($count == 0) { |
806 | return null; | |
807 | } | |
808 | return $sum / $count; | |
76cacec8 | 809 | |
ee1fb969 | 810 | case QUIZ_GRADEHIGHEST: |
13bbfebc | 811 | default: |
25302dee | 812 | $max = null; |
76cacec8 | 813 | foreach ($attempts as $attempt) { |
814 | if ($attempt->sumgrades > $max) { | |
815 | $max = $attempt->sumgrades; | |
816 | } | |
817 | } | |
818 | return $max; | |
819 | } | |
820 | } | |
821 | ||
25302dee TH |
822 | /** |
823 | * Update the final grade at this quiz for all students. | |
824 | * | |
825 | * This function is equivalent to calling quiz_save_best_grade for all | |
826 | * users, but much more efficient. | |
827 | * | |
828 | * @param object $quiz the quiz settings. | |
829 | */ | |
830 | function quiz_update_all_final_grades($quiz) { | |
831 | global $DB; | |
832 | ||
833 | if (!$quiz->sumgrades) { | |
834 | return; | |
835 | } | |
836 | ||
be18f589 | 837 | $param = array('iquizid' => $quiz->id, 'istatefinished' => quiz_attempt::FINISHED); |
25302dee TH |
838 | $firstlastattemptjoin = "JOIN ( |
839 | SELECT | |
840 | iquiza.userid, | |
841 | MIN(attempt) AS firstattempt, | |
842 | MAX(attempt) AS lastattempt | |
843 | ||
e50a5813 | 844 | FROM {quiz_attempts} iquiza |
25302dee TH |
845 | |
846 | WHERE | |
07051106 | 847 | iquiza.state = :istatefinished AND |
25302dee TH |
848 | iquiza.preview = 0 AND |
849 | iquiza.quiz = :iquizid | |
850 | ||
851 | GROUP BY iquiza.userid | |
852 | ) first_last_attempts ON first_last_attempts.userid = quiza.userid"; | |
853 | ||
854 | switch ($quiz->grademethod) { | |
855 | case QUIZ_ATTEMPTFIRST: | |
81522abd | 856 | // Because of the where clause, there will only be one row, but we |
25302dee TH |
857 | // must still use an aggregate function. |
858 | $select = 'MAX(quiza.sumgrades)'; | |
859 | $join = $firstlastattemptjoin; | |
860 | $where = 'quiza.attempt = first_last_attempts.firstattempt AND'; | |
861 | break; | |
862 | ||
863 | case QUIZ_ATTEMPTLAST: | |
81522abd | 864 | // Because of the where clause, there will only be one row, but we |
25302dee TH |
865 | // must still use an aggregate function. |
866 | $select = 'MAX(quiza.sumgrades)'; | |
867 | $join = $firstlastattemptjoin; | |
868 | $where = 'quiza.attempt = first_last_attempts.lastattempt AND'; | |
869 | break; | |
870 | ||
871 | case QUIZ_GRADEAVERAGE: | |
872 | $select = 'AVG(quiza.sumgrades)'; | |
873 | $join = ''; | |
874 | $where = ''; | |
875 | break; | |
876 | ||
877 | default: | |
878 | case QUIZ_GRADEHIGHEST: | |
879 | $select = 'MAX(quiza.sumgrades)'; | |
880 | $join = ''; | |
881 | $where = ''; | |
882 | break; | |
883 | } | |
884 | ||
885 | if ($quiz->sumgrades >= 0.000005) { | |
886 | $finalgrade = $select . ' * ' . ($quiz->grade / $quiz->sumgrades); | |
887 | } else { | |
888 | $finalgrade = '0'; | |
889 | } | |
890 | $param['quizid'] = $quiz->id; | |
891 | $param['quizid2'] = $quiz->id; | |
892 | $param['quizid3'] = $quiz->id; | |
893 | $param['quizid4'] = $quiz->id; | |
be18f589 TH |
894 | $param['statefinished'] = quiz_attempt::FINISHED; |
895 | $param['statefinished2'] = quiz_attempt::FINISHED; | |
25302dee TH |
896 | $finalgradesubquery = " |
897 | SELECT quiza.userid, $finalgrade AS newgrade | |
898 | FROM {quiz_attempts} quiza | |
899 | $join | |
900 | WHERE | |
901 | $where | |
a558acf9 | 902 | quiza.state = :statefinished AND |
25302dee TH |
903 | quiza.preview = 0 AND |
904 | quiza.quiz = :quizid3 | |
905 | GROUP BY quiza.userid"; | |
906 | ||
907 | $changedgrades = $DB->get_records_sql(" | |
908 | SELECT users.userid, qg.id, qg.grade, newgrades.newgrade | |
909 | ||
910 | FROM ( | |
911 | SELECT userid | |
912 | FROM {quiz_grades} qg | |
913 | WHERE quiz = :quizid | |
914 | UNION | |
915 | SELECT DISTINCT userid | |
916 | FROM {quiz_attempts} quiza2 | |
917 | WHERE | |
a558acf9 | 918 | quiza2.state = :statefinished2 AND |
25302dee TH |
919 | quiza2.preview = 0 AND |
920 | quiza2.quiz = :quizid2 | |
921 | ) users | |
922 | ||
923 | LEFT JOIN {quiz_grades} qg ON qg.userid = users.userid AND qg.quiz = :quizid4 | |
924 | ||
925 | LEFT JOIN ( | |
926 | $finalgradesubquery | |
927 | ) newgrades ON newgrades.userid = users.userid | |
928 | ||
929 | WHERE | |
930 | ABS(newgrades.newgrade - qg.grade) > 0.000005 OR | |
f484a629 TH |
931 | ((newgrades.newgrade IS NULL OR qg.grade IS NULL) AND NOT |
932 | (newgrades.newgrade IS NULL AND qg.grade IS NULL))", | |
933 | // The mess on the previous line is detecting where the value is | |
934 | // NULL in one column, and NOT NULL in the other, but SQL does | |
935 | // not have an XOR operator, and MS SQL server can't cope with | |
936 | // (newgrades.newgrade IS NULL) <> (qg.grade IS NULL). | |
a13d4fbd | 937 | $param); |
25302dee TH |
938 | |
939 | $timenow = time(); | |
940 | $todelete = array(); | |
941 | foreach ($changedgrades as $changedgrade) { | |
942 | ||
943 | if (is_null($changedgrade->newgrade)) { | |
944 | $todelete[] = $changedgrade->userid; | |
945 | ||
946 | } else if (is_null($changedgrade->grade)) { | |
0ff4bd08 | 947 | $toinsert = new stdClass(); |
25302dee TH |
948 | $toinsert->quiz = $quiz->id; |
949 | $toinsert->userid = $changedgrade->userid; | |
950 | $toinsert->timemodified = $timenow; | |
951 | $toinsert->grade = $changedgrade->newgrade; | |
952 | $DB->insert_record('quiz_grades', $toinsert); | |
953 | ||
954 | } else { | |
0ff4bd08 | 955 | $toupdate = new stdClass(); |
25302dee TH |
956 | $toupdate->id = $changedgrade->id; |
957 | $toupdate->grade = $changedgrade->newgrade; | |
958 | $toupdate->timemodified = $timenow; | |
959 | $DB->update_record('quiz_grades', $toupdate); | |
960 | } | |
961 | } | |
962 | ||
963 | if (!empty($todelete)) { | |
e65f89b9 | 964 | list($test, $params) = $DB->get_in_or_equal($todelete); |
8d13ea83 TH |
965 | $DB->delete_records_select('quiz_grades', 'quiz = ? AND userid ' . $test, |
966 | array_merge(array($quiz->id), $params)); | |
25302dee TH |
967 | } |
968 | } | |
969 | ||
8e771aed MP |
970 | /** |
971 | * Efficiently update check state time on all open attempts | |
972 | * | |
973 | * @param array $conditions optional restrictions on which attempts to update | |
974 | * Allowed conditions: | |
975 | * courseid => (array|int) attempts in given course(s) | |
976 | * userid => (array|int) attempts for given user(s) | |
977 | * quizid => (array|int) attempts in given quiz(s) | |
978 | * groupid => (array|int) quizzes with some override for given group(s) | |
979 | * | |
980 | */ | |
981 | function quiz_update_open_attempts(array $conditions) { | |
982 | global $DB; | |
983 | ||
984 | foreach ($conditions as &$value) { | |
985 | if (!is_array($value)) { | |
986 | $value = array($value); | |
987 | } | |
988 | } | |
989 | ||
990 | $params = array(); | |
805e32f7 TH |
991 | $wheres = array("quiza.state IN ('inprogress', 'overdue')"); |
992 | $iwheres = array("iquiza.state IN ('inprogress', 'overdue')"); | |
8e771aed MP |
993 | |
994 | if (isset($conditions['courseid'])) { | |
995 | list ($incond, $inparams) = $DB->get_in_or_equal($conditions['courseid'], SQL_PARAMS_NAMED, 'cid'); | |
996 | $params = array_merge($params, $inparams); | |
805e32f7 TH |
997 | $wheres[] = "quiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)"; |
998 | list ($incond, $inparams) = $DB->get_in_or_equal($conditions['courseid'], SQL_PARAMS_NAMED, 'icid'); | |
999 | $params = array_merge($params, $inparams); | |
1000 | $iwheres[] = "iquiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)"; | |
8e771aed | 1001 | } |
805e32f7 | 1002 | |
8e771aed MP |
1003 | if (isset($conditions['userid'])) { |
1004 | list ($incond, $inparams) = $DB->get_in_or_equal($conditions['userid'], SQL_PARAMS_NAMED, 'uid'); | |
1005 | $params = array_merge($params, $inparams); | |
805e32f7 TH |
1006 | $wheres[] = "quiza.userid $incond"; |
1007 | list ($incond, $inparams) = $DB->get_in_or_equal($conditions['userid'], SQL_PARAMS_NAMED, 'iuid'); | |
1008 | $params = array_merge($params, $inparams); | |
1009 | $iwheres[] = "iquiza.userid $incond"; | |
8e771aed | 1010 | } |
805e32f7 | 1011 | |
8e771aed MP |
1012 | if (isset($conditions['quizid'])) { |
1013 | list ($incond, $inparams) = $DB->get_in_or_equal($conditions['quizid'], SQL_PARAMS_NAMED, 'qid'); | |
1014 | $params = array_merge($params, $inparams); | |
805e32f7 TH |
1015 | $wheres[] = "quiza.quiz $incond"; |
1016 | list ($incond, $inparams) = $DB->get_in_or_equal($conditions['quizid'], SQL_PARAMS_NAMED, 'iqid'); | |
1017 | $params = array_merge($params, $inparams); | |
1018 | $iwheres[] = "iquiza.quiz $incond"; | |
8e771aed | 1019 | } |
805e32f7 | 1020 | |
8e771aed MP |
1021 | if (isset($conditions['groupid'])) { |
1022 | list ($incond, $inparams) = $DB->get_in_or_equal($conditions['groupid'], SQL_PARAMS_NAMED, 'gid'); | |
1023 | $params = array_merge($params, $inparams); | |
805e32f7 TH |
1024 | $wheres[] = "quiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)"; |
1025 | list ($incond, $inparams) = $DB->get_in_or_equal($conditions['groupid'], SQL_PARAMS_NAMED, 'igid'); | |
1026 | $params = array_merge($params, $inparams); | |
1027 | $iwheres[] = "iquiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)"; | |
8e771aed MP |
1028 | } |
1029 | ||
1030 | // SQL to compute timeclose and timelimit for each attempt: | |
805e32f7 TH |
1031 | $quizausersql = quiz_get_attempt_usertime_sql( |
1032 | implode("\n AND ", $iwheres)); | |
8e771aed MP |
1033 | |
1034 | // SQL to compute the new timecheckstate | |
1035 | $timecheckstatesql = " | |
1036 | CASE WHEN quizauser.usertimelimit = 0 AND quizauser.usertimeclose = 0 THEN NULL | |
1037 | WHEN quizauser.usertimelimit = 0 THEN quizauser.usertimeclose | |
1038 | WHEN quizauser.usertimeclose = 0 THEN quiza.timestart + quizauser.usertimelimit | |
1039 | WHEN quiza.timestart + quizauser.usertimelimit < quizauser.usertimeclose THEN quiza.timestart + quizauser.usertimelimit | |
1040 | ELSE quizauser.usertimeclose END + | |
1041 | CASE WHEN quiza.state = 'overdue' THEN quiz.graceperiod ELSE 0 END"; | |
1042 | ||
1043 | // SQL to select which attempts to process | |
805e32f7 | 1044 | $attemptselect = implode("\n AND ", $wheres); |
8e771aed MP |
1045 | |
1046 | /* | |
1047 | * Each database handles updates with inner joins differently: | |
1048 | * - mysql does not allow a FROM clause | |
1049 | * - postgres and mssql allow FROM but handle table aliases differently | |
1050 | * - oracle requires a subquery | |
1051 | * | |
1052 | * Different code for each database. | |
1053 | */ | |
1054 | ||
1055 | $dbfamily = $DB->get_dbfamily(); | |
1056 | if ($dbfamily == 'mysql') { | |
1057 | $updatesql = "UPDATE {quiz_attempts} quiza | |
1058 | JOIN {quiz} quiz ON quiz.id = quiza.quiz | |
1059 | JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id | |
1060 | SET quiza.timecheckstate = $timecheckstatesql | |
1061 | WHERE $attemptselect"; | |
1062 | } else if ($dbfamily == 'postgres') { | |
1063 | $updatesql = "UPDATE {quiz_attempts} quiza | |
1064 | SET timecheckstate = $timecheckstatesql | |
1065 | FROM {quiz} quiz, ( $quizausersql ) quizauser | |
1066 | WHERE quiz.id = quiza.quiz | |
1067 | AND quizauser.id = quiza.id | |
1068 | AND $attemptselect"; | |
1069 | } else if ($dbfamily == 'mssql') { | |
1070 | $updatesql = "UPDATE quiza | |
1071 | SET timecheckstate = $timecheckstatesql | |
1072 | FROM {quiz_attempts} quiza | |
1073 | JOIN {quiz} quiz ON quiz.id = quiza.quiz | |
1074 | JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id | |
1075 | WHERE $attemptselect"; | |
1076 | } else { | |
1077 | // oracle, sqlite and others | |
1078 | $updatesql = "UPDATE {quiz_attempts} quiza | |
1079 | SET timecheckstate = ( | |
1080 | SELECT $timecheckstatesql | |
1081 | FROM {quiz} quiz, ( $quizausersql ) quizauser | |
1082 | WHERE quiz.id = quiza.quiz | |
1083 | AND quizauser.id = quiza.id | |
1084 | ) | |
1085 | WHERE $attemptselect"; | |
1086 | } | |
1087 | ||
1088 | $DB->execute($updatesql, $params); | |
1089 | } | |
1090 | ||
1091 | /** | |
1092 | * Returns SQL to compute timeclose and timelimit for every attempt, taking into account user and group overrides. | |
1093 | * | |
805e32f7 TH |
1094 | * @param string $redundantwhereclauses extra where clauses to add to the subquery |
1095 | * for performance. These can use the table alias iquiza for the quiz attempts table. | |
1096 | * @return string SQL select with columns attempt.id, usertimeclose, usertimelimit. | |
8e771aed | 1097 | */ |
805e32f7 TH |
1098 | function quiz_get_attempt_usertime_sql($redundantwhereclauses = '') { |
1099 | if ($redundantwhereclauses) { | |
1100 | $redundantwhereclauses = 'WHERE ' . $redundantwhereclauses; | |
1101 | } | |
8e771aed MP |
1102 | // The multiple qgo JOINS are necessary because we want timeclose/timelimit = 0 (unlimited) to supercede |
1103 | // any other group override | |
1104 | $quizausersql = " | |
1105 | SELECT iquiza.id, | |
1106 | COALESCE(MAX(quo.timeclose), MAX(qgo1.timeclose), MAX(qgo2.timeclose), iquiz.timeclose) AS usertimeclose, | |
1107 | COALESCE(MAX(quo.timelimit), MAX(qgo3.timelimit), MAX(qgo4.timelimit), iquiz.timelimit) AS usertimelimit | |
1108 | ||
1109 | FROM {quiz_attempts} iquiza | |
1110 | JOIN {quiz} iquiz ON iquiz.id = iquiza.quiz | |
1111 | LEFT JOIN {quiz_overrides} quo ON quo.quiz = iquiza.quiz AND quo.userid = iquiza.userid | |
1112 | LEFT JOIN {groups_members} gm ON gm.userid = iquiza.userid | |
1113 | LEFT JOIN {quiz_overrides} qgo1 ON qgo1.quiz = iquiza.quiz AND qgo1.groupid = gm.groupid AND qgo1.timeclose = 0 | |
1114 | LEFT JOIN {quiz_overrides} qgo2 ON qgo2.quiz = iquiza.quiz AND qgo2.groupid = gm.groupid AND qgo2.timeclose > 0 | |
1115 | LEFT JOIN {quiz_overrides} qgo3 ON qgo3.quiz = iquiza.quiz AND qgo3.groupid = gm.groupid AND qgo3.timelimit = 0 | |
1116 | LEFT JOIN {quiz_overrides} qgo4 ON qgo4.quiz = iquiza.quiz AND qgo4.groupid = gm.groupid AND qgo4.timelimit > 0 | |
805e32f7 | 1117 | $redundantwhereclauses |
8e771aed MP |
1118 | GROUP BY iquiza.id, iquiz.id, iquiz.timeclose, iquiz.timelimit"; |
1119 | return $quizausersql; | |
1120 | } | |
1121 | ||
fc44ee0d | 1122 | /** |
26d12a0c | 1123 | * Return the attempt with the best grade for a quiz |
1124 | * | |
1125 | * Which attempt is the best depends on $quiz->grademethod. If the grade | |
1126 | * method is GRADEAVERAGE then this function simply returns the last attempt. | |
1127 | * @return object The attempt with the best grade | |
1128 | * @param object $quiz The quiz for which the best grade is to be calculated | |
1129 | * @param array $attempts An array of all the attempts of the user at the quiz | |
1130 | */ | |
76cacec8 | 1131 | function quiz_calculate_best_attempt($quiz, $attempts) { |
76cacec8 | 1132 | |
1133 | switch ($quiz->grademethod) { | |
1134 | ||
ee1fb969 | 1135 | case QUIZ_ATTEMPTFIRST: |
76cacec8 | 1136 | foreach ($attempts as $attempt) { |
1137 | return $attempt; | |
1138 | } | |
1139 | break; | |
1140 | ||
9e83f3d1 | 1141 | case QUIZ_GRADEAVERAGE: // We need to do something with it. |
ee1fb969 | 1142 | case QUIZ_ATTEMPTLAST: |
76cacec8 | 1143 | foreach ($attempts as $attempt) { |
1144 | $final = $attempt; | |
1145 | } | |
1146 | return $final; | |
1147 | ||
1148 | default: | |
ee1fb969 | 1149 | case QUIZ_GRADEHIGHEST: |
76cacec8 | 1150 | $max = -1; |
1151 | foreach ($attempts as $attempt) { | |
1152 | if ($attempt->sumgrades > $max) { | |
1153 | $max = $attempt->sumgrades; | |
1154 | $maxattempt = $attempt; | |
1155 | } | |
1156 | } | |
1157 | return $maxattempt; | |
1158 | } | |
1159 | } | |
1160 | ||
25302dee | 1161 | /** |
87427d9c TH |
1162 | * @return array int => lang string the options for calculating the quiz grade |
1163 | * from the individual attempt grades. | |
25302dee TH |
1164 | */ |
1165 | function quiz_get_grading_options() { | |
1166 | return array( | |
1167 | QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'), | |
1168 | QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'), | |
1169 | QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'), | |
1170 | QUIZ_ATTEMPTLAST => get_string('attemptlast', 'quiz') | |
1171 | ); | |
1172 | } | |
1173 | ||
0675e5eb | 1174 | /** |
25a03faa TH |
1175 | * @param int $option one of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE, |
1176 | * QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST. | |
0675e5eb | 1177 | * @return the lang string for that option. |
1178 | */ | |
1179 | function quiz_get_grading_option_name($option) { | |
1180 | $strings = quiz_get_grading_options(); | |
1181 | return $strings[$option]; | |
1182 | } | |
1183 | ||
87427d9c TH |
1184 | /** |
1185 | * @return array string => lang string the options for handling overdue quiz | |
1186 | * attempts. | |
1187 | */ | |
1188 | function quiz_get_overdue_handling_options() { | |
1189 | return array( | |
1190 | 'autosubmit' => get_string('overduehandlingautosubmit', 'quiz'), | |
1191 | 'graceperiod' => get_string('overduehandlinggraceperiod', 'quiz'), | |
1192 | 'autoabandon' => get_string('overduehandlingautoabandon', 'quiz'), | |
1193 | ); | |
1194 | } | |
1195 | ||
9ec7a716 | 1196 | /** |
31805089 | 1197 | * Get the choices for what size user picture to show. |
9ec7a716 TH |
1198 | * @return array string => lang string the options for whether to display the user's picture. |
1199 | */ | |
1200 | function quiz_get_user_image_options() { | |
1201 | return array( | |
1202 | QUIZ_SHOWIMAGE_NONE => get_string('shownoimage', 'quiz'), | |
1203 | QUIZ_SHOWIMAGE_SMALL => get_string('showsmallimage', 'quiz'), | |
1204 | QUIZ_SHOWIMAGE_LARGE => get_string('showlargeimage', 'quiz'), | |
1205 | ); | |
1206 | } | |
1207 | ||
e1a2d0d9 CC |
1208 | /** |
1209 | * Get the choices to offer for the 'Questions per page' option. | |
1210 | * @return array int => string. | |
1211 | */ | |
1212 | function quiz_questions_per_page_options() { | |
1213 | $pageoptions = array(); | |
1214 | $pageoptions[0] = get_string('neverallononepage', 'quiz'); | |
1215 | $pageoptions[1] = get_string('everyquestion', 'quiz'); | |
1216 | for ($i = 2; $i <= QUIZ_MAX_QPP_OPTION; ++$i) { | |
1217 | $pageoptions[$i] = get_string('everynquestions', 'quiz', $i); | |
1218 | } | |
1219 | return $pageoptions; | |
1220 | } | |
1221 | ||
be18f589 | 1222 | /** |
31805089 TH |
1223 | * Get the human-readable name for a quiz attempt state. |
1224 | * @param string $state one of the state constants like {@link quiz_attempt::IN_PROGRESS}. | |
1225 | * @return string The lang string to describe that state. | |
be18f589 TH |
1226 | */ |
1227 | function quiz_attempt_state_name($state) { | |
1228 | switch ($state) { | |
1229 | case quiz_attempt::IN_PROGRESS: | |
1230 | return get_string('stateinprogress', 'quiz'); | |
1231 | case quiz_attempt::OVERDUE: | |
1232 | return get_string('stateoverdue', 'quiz'); | |
1233 | case quiz_attempt::FINISHED: | |
1234 | return get_string('statefinished', 'quiz'); | |
1235 | case quiz_attempt::ABANDONED: | |
1236 | return get_string('stateabandoned', 'quiz'); | |
1237 | default: | |
1238 | throw new coding_exception('Unknown quiz attempt state.'); | |
1239 | } | |
1240 | } | |
1241 | ||
9e83f3d1 | 1242 | // Other quiz functions //////////////////////////////////////////////////////// |
ee1fb969 | 1243 | |
26a29b1a | 1244 | /** |
2a874d65 | 1245 | * @param object $quiz the quiz. |
f7970e3c | 1246 | * @param int $cmid the course_module object for this quiz. |
2a874d65 | 1247 | * @param object $question the question. |
26a29b1a | 1248 | * @param string $returnurl url to return to after action is done. |
de3b803d | 1249 | * @param int $variant which question variant to preview (optional). |
26a29b1a | 1250 | * @return string html for a number of icons linked to action pages for a |
1251 | * question - preview and edit / view icons depending on user capabilities. | |
1252 | */ | |
de3b803d TH |
1253 | function quiz_question_action_icons($quiz, $cmid, $question, $returnurl, $variant = null) { |
1254 | $html = quiz_question_preview_button($quiz, $question, false, $variant) . ' ' . | |
2a874d65 | 1255 | quiz_question_edit_button($cmid, $question, $returnurl); |
1256 | return $html; | |
1257 | } | |
1258 | ||
1259 | /** | |
f7970e3c | 1260 | * @param int $cmid the course_module.id for this quiz. |
2a874d65 | 1261 | * @param object $question the question. |
1262 | * @param string $returnurl url to return to after action is done. | |
1263 | * @param string $contentbeforeicon some HTML content to be added inside the link, before the icon. | |
25a03faa TH |
1264 | * @return the HTML for an edit icon, view icon, or nothing for a question |
1265 | * (depending on permissions). | |
2a874d65 | 1266 | */ |
d04f623b | 1267 | function quiz_question_edit_button($cmid, $question, $returnurl, $contentaftericon = '') { |
d436d197 | 1268 | global $CFG, $OUTPUT; |
2a874d65 | 1269 | |
1270 | // Minor efficiency saving. Only get strings once, even if there are a lot of icons on one page. | |
26a29b1a | 1271 | static $stredit = null; |
1272 | static $strview = null; | |
25a03faa | 1273 | if ($stredit === null) { |
26a29b1a | 1274 | $stredit = get_string('edit'); |
1275 | $strview = get_string('view'); | |
1276 | } | |
2a874d65 | 1277 | |
1278 | // What sort of icon should we show? | |
1279 | $action = ''; | |
25a03faa TH |
1280 | if (!empty($question->id) && |
1281 | (question_has_capability_on($question, 'edit', $question->category) || | |
1282 | question_has_capability_on($question, 'move', $question->category))) { | |
2a874d65 | 1283 | $action = $stredit; |
663640f5 | 1284 | $icon = 't/edit'; |
25a03faa TH |
1285 | } else if (!empty($question->id) && |
1286 | question_has_capability_on($question, 'view', $question->category)) { | |
2a874d65 | 1287 | $action = $strview; |
663640f5 | 1288 | $icon = 'i/info'; |
2a874d65 | 1289 | } |
1290 | ||
1291 | // Build the icon. | |
1292 | if ($action) { | |
47678d60 | 1293 | if ($returnurl instanceof moodle_url) { |
a530d4a9 | 1294 | $returnurl = $returnurl->out_as_local_url(false); |
47678d60 | 1295 | } |
2a874d65 | 1296 | $questionparams = array('returnurl' => $returnurl, 'cmid' => $cmid, 'id' => $question->id); |
1297 | $questionurl = new moodle_url("$CFG->wwwroot/question/question.php", $questionparams); | |
663640f5 DW |
1298 | return '<a title="' . $action . '" href="' . $questionurl->out() . '" class="questioneditbutton">' . |
1299 | $OUTPUT->pix_icon($icon, $action) . $contentaftericon . | |
d04f623b | 1300 | '</a>'; |
068384ce TH |
1301 | } else if ($contentaftericon) { |
1302 | return '<span class="questioneditbutton">' . $contentaftericon . '</span>'; | |
2a874d65 | 1303 | } else { |
068384ce | 1304 | return ''; |
26a29b1a | 1305 | } |
26a29b1a | 1306 | } |
1307 | ||
7d87171b | 1308 | /** |
612106b3 TH |
1309 | * @param object $quiz the quiz settings |
1310 | * @param object $question the question | |
de3b803d | 1311 | * @param int $variant which question variant to preview (optional). |
612106b3 TH |
1312 | * @return moodle_url to preview this question with the options from this quiz. |
1313 | */ | |
de3b803d | 1314 | function quiz_question_preview_url($quiz, $question, $variant = null) { |
612106b3 TH |
1315 | // Get the appropriate display options. |
1316 | $displayoptions = mod_quiz_display_options::make_from_quiz($quiz, | |
1317 | mod_quiz_display_options::DURING); | |
1318 | ||
1319 | $maxmark = null; | |
1320 | if (isset($question->maxmark)) { | |
1321 | $maxmark = $question->maxmark; | |
1322 | } | |
1323 | ||
1324 | // Work out the correcte preview URL. | |
1325 | return question_preview_url($question->id, $quiz->preferredbehaviour, | |
de3b803d | 1326 | $maxmark, $displayoptions, $variant); |
612106b3 TH |
1327 | } |
1328 | ||
1329 | /** | |
1330 | * @param object $quiz the quiz settings | |
7d87171b | 1331 | * @param object $question the question |
f7970e3c | 1332 | * @param bool $label if true, show the preview question label after the icon |
de3b803d | 1333 | * @param int $variant which question variant to preview (optional). |
7d87171b | 1334 | * @return the HTML for a preview question icon. |
1335 | */ | |
de3b803d | 1336 | function quiz_question_preview_button($quiz, $question, $label = false, $variant = null) { |
e1a2d0d9 | 1337 | global $PAGE; |
2a874d65 | 1338 | if (!question_has_capability_on($question, 'use', $question->category)) { |
271e6dec | 1339 | return ''; |
1340 | } | |
2a874d65 | 1341 | |
de3b803d | 1342 | return $PAGE->get_renderer('mod_quiz', 'edit')->question_preview_icon($quiz, $question, $label, $variant); |
ad7e7ba8 | 1343 | } |
0d156caa | 1344 | |
62e76c67 | 1345 | /** |
1346 | * @param object $attempt the attempt. | |
1347 | * @param object $context the quiz context. | |
f7970e3c | 1348 | * @return int whether flags should be shown/editable to the current user for this attempt. |
62e76c67 | 1349 | */ |
1350 | function quiz_get_flag_option($attempt, $context) { | |
1351 | global $USER; | |
25302dee TH |
1352 | if (!has_capability('moodle/question:flag', $context)) { |
1353 | return question_display_options::HIDDEN; | |
1354 | } else if ($attempt->userid == $USER->id) { | |
1355 | return question_display_options::EDITABLE; | |
1356 | } else { | |
1357 | return question_display_options::VISIBLE; | |
62e76c67 | 1358 | } |
62e76c67 | 1359 | } |
1360 | ||
4f48fb42 | 1361 | /** |
be18f589 TH |
1362 | * Work out what state this quiz attempt is in - in the sense used by |
1363 | * quiz_get_review_options, not in the sense of $attempt->state. | |
25302dee TH |
1364 | * @param object $quiz the quiz settings |
1365 | * @param object $attempt the quiz_attempt database row. | |
f7970e3c | 1366 | * @return int one of the mod_quiz_display_options::DURING, |
25302dee | 1367 | * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. |
4dca7e51 | 1368 | */ |
25302dee | 1369 | function quiz_attempt_state($quiz, $attempt) { |
b3c18b86 | 1370 | if ($attempt->state == quiz_attempt::IN_PROGRESS) { |
25302dee | 1371 | return mod_quiz_display_options::DURING; |
d0f8daef TH |
1372 | } else if ($quiz->timeclose && time() >= $quiz->timeclose) { |
1373 | return mod_quiz_display_options::AFTER_CLOSE; | |
25302dee TH |
1374 | } else if (time() < $attempt->timefinish + 120) { |
1375 | return mod_quiz_display_options::IMMEDIATELY_AFTER; | |
25302dee | 1376 | } else { |
d0f8daef | 1377 | return mod_quiz_display_options::LATER_WHILE_OPEN; |
25302dee | 1378 | } |
4f48fb42 | 1379 | } |
1380 | ||
4f48fb42 | 1381 | /** |
25302dee TH |
1382 | * The the appropraite mod_quiz_display_options object for this attempt at this |
1383 | * quiz right now. | |
77ed3ba5 | 1384 | * |
cd06115f | 1385 | * @param object $quiz the quiz instance. |
1386 | * @param object $attempt the attempt in question. | |
25302dee | 1387 | * @param $context the quiz context. |
77ed3ba5 | 1388 | * |
25302dee | 1389 | * @return mod_quiz_display_options |
cd06115f | 1390 | */ |
7ee80cab | 1391 | function quiz_get_review_options($quiz, $attempt, $context) { |
25302dee | 1392 | $options = mod_quiz_display_options::make_from_quiz($quiz, quiz_attempt_state($quiz, $attempt)); |
62e76c67 | 1393 | |
4f48fb42 | 1394 | $options->readonly = true; |
62e76c67 | 1395 | $options->flags = quiz_get_flag_option($attempt, $context); |
2709ee45 TH |
1396 | if (!empty($attempt->id)) { |
1397 | $options->questionreviewlink = new moodle_url('/mod/quiz/reviewquestion.php', | |
1398 | array('attempt' => $attempt->id)); | |
1399 | } | |
b6e907a2 | 1400 | |
9e83f3d1 | 1401 | // Show a link to the comment box only for closed attempts. |
be18f589 | 1402 | if (!empty($attempt->id) && $attempt->state == quiz_attempt::FINISHED && !$attempt->preview && |
2709ee45 | 1403 | !is_null($context) && has_capability('mod/quiz:grade', $context)) { |
25302dee | 1404 | $options->manualcomment = question_display_options::VISIBLE; |
7ee80cab TH |
1405 | $options->manualcommentlink = new moodle_url('/mod/quiz/comment.php', |
1406 | array('attempt' => $attempt->id)); | |
01600b51 | 1407 | } |
1408 | ||
25a03faa TH |
1409 | if (!is_null($context) && !$attempt->preview && |
1410 | has_capability('mod/quiz:viewreports', $context) && | |
25302dee | 1411 | has_capability('moodle/grade:viewhidden', $context)) { |
e2833e87 | 1412 | // People who can see reports and hidden grades should be shown everything, |
1413 | // except during preview when teachers want to see what students see. | |
25302dee TH |
1414 | $options->attempt = question_display_options::VISIBLE; |
1415 | $options->correctness = question_display_options::VISIBLE; | |
1416 | $options->marks = question_display_options::MARK_AND_MAX; | |
1417 | $options->feedback = question_display_options::VISIBLE; | |
1418 | $options->numpartscorrect = question_display_options::VISIBLE; | |
e101ec9f | 1419 | $options->manualcomment = question_display_options::VISIBLE; |
25302dee TH |
1420 | $options->generalfeedback = question_display_options::VISIBLE; |
1421 | $options->rightanswer = question_display_options::VISIBLE; | |
1422 | $options->overallfeedback = question_display_options::VISIBLE; | |
1423 | $options->history = question_display_options::VISIBLE; | |
e2833e87 | 1424 | |
4f48fb42 | 1425 | } |
77ed3ba5 | 1426 | |
4f48fb42 | 1427 | return $options; |
1428 | } | |
77ed3ba5 | 1429 | |
40377b18 | 1430 | /** |
77ed3ba5 | 1431 | * Combines the review options from a number of different quiz attempts. |
25302dee | 1432 | * Returns an array of two ojects, so the suggested way of calling this |
77ed3ba5 | 1433 | * funciton is: |
1434 | * list($someoptions, $alloptions) = quiz_get_combined_reviewoptions(...) | |
1435 | * | |
1436 | * @param object $quiz the quiz instance. | |
1437 | * @param array $attempts an array of attempt objects. | |
77ed3ba5 | 1438 | * |
1439 | * @return array of two options objects, one showing which options are true for | |
1440 | * at least one of the attempts, the other showing which options are true | |
1441 | * for all attempts. | |
1442 | */ | |
25302dee | 1443 | function quiz_get_combined_reviewoptions($quiz, $attempts) { |
6d03fd98 | 1444 | $fields = array('feedback', 'generalfeedback', 'rightanswer', 'overallfeedback'); |
0ff4bd08 TH |
1445 | $someoptions = new stdClass(); |
1446 | $alloptions = new stdClass(); | |
77ed3ba5 | 1447 | foreach ($fields as $field) { |
1448 | $someoptions->$field = false; | |
1449 | $alloptions->$field = true; | |
40377b18 | 1450 | } |
6d03fd98 TH |
1451 | $someoptions->marks = question_display_options::HIDDEN; |
1452 | $alloptions->marks = question_display_options::MARK_AND_MAX; | |
1453 | ||
c76425e8 JL |
1454 | // This shouldn't happen, but we need to prevent reveal information. |
1455 | if (empty($attempts)) { | |
1456 | return array($someoptions, $someoptions); | |
1457 | } | |
1458 | ||
77ed3ba5 | 1459 | foreach ($attempts as $attempt) { |
25302dee TH |
1460 | $attemptoptions = mod_quiz_display_options::make_from_quiz($quiz, |
1461 | quiz_attempt_state($quiz, $attempt)); | |
7d014bf5 | 1462 | foreach ($fields as $field) { |
1463 | $someoptions->$field = $someoptions->$field || $attemptoptions->$field; | |
1464 | $alloptions->$field = $alloptions->$field && $attemptoptions->$field; | |
1465 | } | |
6d03fd98 TH |
1466 | $someoptions->marks = max($someoptions->marks, $attemptoptions->marks); |
1467 | $alloptions->marks = min($alloptions->marks, $attemptoptions->marks); | |
77ed3ba5 | 1468 | } |
1469 | return array($someoptions, $alloptions); | |
40377b18 | 1470 | } |
ac48e43a | 1471 | |
9e83f3d1 | 1472 | // Functions for sending notification messages ///////////////////////////////// |
ac48e43a | 1473 | |
1474 | /** | |
cbb8b55c | 1475 | * Sends a confirmation message to the student confirming that the attempt was processed. |
ac48e43a | 1476 | * |
cbb8b55c TH |
1477 | * @param object $a lots of useful information that can be used in the message |
1478 | * subject and body. | |
ac48e43a | 1479 | * |
cbb8b55c | 1480 | * @return int|false as for {@link message_send()}. |
ac48e43a | 1481 | */ |
cbb8b55c | 1482 | function quiz_send_confirmation($recipient, $a) { |
ac48e43a | 1483 | |
9e83f3d1 | 1484 | // Add information about the recipient to $a. |
cbb8b55c TH |
1485 | // Don't do idnumber. we want idnumber to be the submitter's idnumber. |
1486 | $a->username = fullname($recipient); | |
1487 | $a->userusername = $recipient->username; | |
ac48e43a | 1488 | |
9e83f3d1 | 1489 | // Prepare the message. |
cc350fd9 AD |
1490 | $eventdata = new \core\message\message(); |
1491 | $eventdata->courseid = $a->courseid; | |
cbb8b55c TH |
1492 | $eventdata->component = 'mod_quiz'; |
1493 | $eventdata->name = 'confirmation'; | |
94263cca AD |
1494 | $eventdata->notification = 1; |
1495 | ||
5540b2ca | 1496 | $eventdata->userfrom = core_user::get_noreply_user(); |
cbb8b55c TH |
1497 | $eventdata->userto = $recipient; |
1498 | $eventdata->subject = get_string('emailconfirmsubject', 'quiz', $a); | |
1499 | $eventdata->fullmessage = get_string('emailconfirmbody', 'quiz', $a); | |
3b120e46 | 1500 | $eventdata->fullmessageformat = FORMAT_PLAIN; |
1501 | $eventdata->fullmessagehtml = ''; | |
94263cca AD |
1502 | |
1503 | $eventdata->smallmessage = get_string('emailconfirmsmall', 'quiz', $a); | |
1504 | $eventdata->contexturl = $a->quizurl; | |
1505 | $eventdata->contexturlname = $a->quizname; | |
99d19c13 | 1506 | |
cbb8b55c TH |
1507 | // ... and send it. |
1508 | return message_send($eventdata); | |
ac48e43a | 1509 | } |
1510 | ||
1511 | /** | |
bc68fc9a | 1512 | * Sends notification messages to the interested parties that assign the role capability |
ac48e43a | 1513 | * |
1514 | * @param object $recipient user object of the intended recipient | |
0ff4bd08 | 1515 | * @param object $a associative array of replaceable fields for the templates |
ac48e43a | 1516 | * |
cbb8b55c | 1517 | * @return int|false as for {@link message_send()}. |
ac48e43a | 1518 | */ |
cbb8b55c | 1519 | function quiz_send_notification($recipient, $submitter, $a) { |
ac48e43a | 1520 | |
9e83f3d1 | 1521 | // Recipient info for template. |
cbb8b55c TH |
1522 | $a->useridnumber = $recipient->idnumber; |
1523 | $a->username = fullname($recipient); | |
ac48e43a | 1524 | $a->userusername = $recipient->username; |
ac48e43a | 1525 | |
9e83f3d1 | 1526 | // Prepare the message. |
cc350fd9 AD |
1527 | $eventdata = new \core\message\message(); |
1528 | $eventdata->courseid = $a->courseid; | |
cbb8b55c TH |
1529 | $eventdata->component = 'mod_quiz'; |
1530 | $eventdata->name = 'submission'; | |
bc68fc9a AD |
1531 | $eventdata->notification = 1; |
1532 | ||
cbb8b55c | 1533 | $eventdata->userfrom = $submitter; |
3b120e46 | 1534 | $eventdata->userto = $recipient; |
cbb8b55c TH |
1535 | $eventdata->subject = get_string('emailnotifysubject', 'quiz', $a); |
1536 | $eventdata->fullmessage = get_string('emailnotifybody', 'quiz', $a); | |
3b120e46 | 1537 | $eventdata->fullmessageformat = FORMAT_PLAIN; |
1538 | $eventdata->fullmessagehtml = ''; | |
bc68fc9a AD |
1539 | |
1540 | $eventdata->smallmessage = get_string('emailnotifysmall', 'quiz', $a); | |
1541 | $eventdata->contexturl = $a->quizreviewurl; | |
1542 | $eventdata->contexturlname = $a->quizname; | |
1543 | ||
cbb8b55c TH |
1544 | // ... and send it. |
1545 | return message_send($eventdata); | |
ac48e43a | 1546 | } |
1547 | ||
1548 | /** | |
cbb8b55c | 1549 | * Send all the requried messages when a quiz attempt is submitted. |
ac48e43a | 1550 | * |
1551 | * @param object $course the course | |
1552 | * @param object $quiz the quiz | |
1553 | * @param object $attempt this attempt just finished | |
1554 | * @param object $context the quiz context | |
1555 | * @param object $cm the coursemodule for this quiz | |
1556 | * | |
cbb8b55c | 1557 | * @return bool true if all necessary messages were sent successfully, else false. |
ac48e43a | 1558 | */ |
cbb8b55c TH |
1559 | function quiz_send_notification_messages($course, $quiz, $attempt, $context, $cm) { |
1560 | global $CFG, $DB; | |
ac48e43a | 1561 | |
9e83f3d1 | 1562 | // Do nothing if required objects not present. |
ac48e43a | 1563 | if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) { |
cbb8b55c | 1564 | throw new coding_exception('$course, $quiz, $attempt, $context and $cm must all be set.'); |
ac48e43a | 1565 | } |
1566 | ||
cbb8b55c TH |
1567 | $submitter = $DB->get_record('user', array('id' => $attempt->userid), '*', MUST_EXIST); |
1568 | ||
9e83f3d1 | 1569 | // Check for confirmation required. |
ac48e43a | 1570 | $sendconfirm = false; |
1571 | $notifyexcludeusers = ''; | |
cbb8b55c TH |
1572 | if (has_capability('mod/quiz:emailconfirmsubmission', $context, $submitter, false)) { |
1573 | $notifyexcludeusers = $submitter->id; | |
ac48e43a | 1574 | $sendconfirm = true; |
1575 | } | |
1576 | ||
9e83f3d1 | 1577 | // Check for notifications required. |
692ef101 AD |
1578 | $notifyfields = 'u.id, u.username, u.idnumber, u.email, u.emailstop, u.lang, |
1579 | u.timezone, u.mailformat, u.maildisplay, u.auth, u.suspended, u.deleted, '; | |
b2b50d92 | 1580 | $notifyfields .= get_all_user_name_fields(true, 'u'); |
efc76e9e | 1581 | $groups = groups_get_all_groups($course->id, $submitter->id, $cm->groupingid); |
5a128471 | 1582 | if (is_array($groups) && count($groups) > 0) { |
1583 | $groups = array_keys($groups); | |
255985f1 | 1584 | } else if (groups_get_activity_groupmode($cm, $course) != NOGROUPS) { |
1585 | // If the user is not in a group, and the quiz is set to group mode, | |
cbb8b55c | 1586 | // then set $groups to a non-existant id so that only users with |
255985f1 | 1587 | // 'moodle/site:accessallgroups' get notified. |
1588 | $groups = -1; | |
5a128471 | 1589 | } else { |
1590 | $groups = ''; | |
1591 | } | |
ac48e43a | 1592 | $userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission', |
5a128471 | 1593 | $notifyfields, '', '', '', $groups, $notifyexcludeusers, false, false, true); |
ac48e43a | 1594 | |
cbb8b55c TH |
1595 | if (empty($userstonotify) && !$sendconfirm) { |
1596 | return true; // Nothing to do. | |
ac48e43a | 1597 | } |
1598 | ||
cbb8b55c | 1599 | $a = new stdClass(); |
9e83f3d1 | 1600 | // Course info. |
cc350fd9 | 1601 | $a->courseid = $course->id; |
cbb8b55c TH |
1602 | $a->coursename = $course->fullname; |
1603 | $a->courseshortname = $course->shortname; | |
9e83f3d1 | 1604 | // Quiz info. |
cbb8b55c TH |
1605 | $a->quizname = $quiz->name; |
1606 | $a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?id=' . $cm->id; | |
1607 | $a->quizreportlink = '<a href="' . $a->quizreporturl . '">' . | |
1608 | format_string($quiz->name) . ' report</a>'; | |
cbb8b55c TH |
1609 | $a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id; |
1610 | $a->quizlink = '<a href="' . $a->quizurl . '">' . format_string($quiz->name) . '</a>'; | |
9e83f3d1 | 1611 | // Attempt info. |
cbb8b55c TH |
1612 | $a->submissiontime = userdate($attempt->timefinish); |
1613 | $a->timetaken = format_time($attempt->timefinish - $attempt->timestart); | |
fd0537ce TH |
1614 | $a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id; |
1615 | $a->quizreviewlink = '<a href="' . $a->quizreviewurl . '">' . | |
1616 | format_string($quiz->name) . ' review</a>'; | |
9e83f3d1 | 1617 | // Student who sat the quiz info. |
cbb8b55c TH |
1618 | $a->studentidnumber = $submitter->idnumber; |
1619 | $a->studentname = fullname($submitter); | |
1620 | $a->studentusername = $submitter->username; | |
1621 | ||
1622 | $allok = true; | |
1623 | ||
9e83f3d1 | 1624 | // Send notifications if required. |
ac48e43a | 1625 | if (!empty($userstonotify)) { |
ac48e43a | 1626 | foreach ($userstonotify as $recipient) { |
cbb8b55c | 1627 | $allok = $allok && quiz_send_notification($recipient, $submitter, $a); |
ac48e43a | 1628 | } |
1629 | } | |
1630 | ||
cbb8b55c TH |
1631 | // Send confirmation if required. We send the student confirmation last, so |
1632 | // that if message sending is being intermittently buggy, which means we send | |
1633 | // some but not all messages, and then try again later, then teachers may get | |
1634 | // duplicate messages, but the student will always get exactly one. | |
1635 | if ($sendconfirm) { | |
1636 | $allok = $allok && quiz_send_confirmation($submitter, $a); | |
1637 | } | |
1638 | ||
1639 | return $allok; | |
1640 | } | |
1641 | ||
fd0537ce TH |
1642 | /** |
1643 | * Send the notification message when a quiz attempt becomes overdue. | |
1644 | * | |
1c2e05c0 | 1645 | * @param quiz_attempt $attemptobj all the data about the quiz attempt. |
fd0537ce | 1646 | */ |
1c2e05c0 | 1647 | function quiz_send_overdue_message($attemptobj) { |
fd0537ce TH |
1648 | global $CFG, $DB; |
1649 | ||
1c2e05c0 | 1650 | $submitter = $DB->get_record('user', array('id' => $attemptobj->get_userid()), '*', MUST_EXIST); |
fd0537ce | 1651 | |
1c2e05c0 TH |
1652 | if (!$attemptobj->has_capability('mod/quiz:emailwarnoverdue', $submitter->id, false)) { |
1653 | return; // Message not required. | |
1654 | } | |
fd0537ce | 1655 | |
1c2e05c0 | 1656 | if (!$attemptobj->has_response_to_at_least_one_graded_question()) { |
fd0537ce TH |
1657 | return; // Message not required. |
1658 | } | |
1659 | ||
1660 | // Prepare lots of useful information that admins might want to include in | |
1661 | // the email message. | |
1c2e05c0 | 1662 | $quizname = format_string($attemptobj->get_quiz_name()); |
fd0537ce TH |
1663 | |
1664 | $deadlines = array(); | |
1c2e05c0 TH |
1665 | if ($attemptobj->get_quiz()->timelimit) { |
1666 | $deadlines[] = $attemptobj->get_attempt()->timestart + $attemptobj->get_quiz()->timelimit; | |
fd0537ce | 1667 | } |
1c2e05c0 TH |
1668 | if ($attemptobj->get_quiz()->timeclose) { |
1669 | $deadlines[] = $attemptobj->get_quiz()->timeclose; | |
fd0537ce | 1670 | } |
59d14f31 | 1671 | $duedate = min($deadlines); |
1c2e05c0 | 1672 | $graceend = $duedate + $attemptobj->get_quiz()->graceperiod; |
fd0537ce TH |
1673 | |
1674 | $a = new stdClass(); | |
1675 | // Course info. | |
cc350fd9 | 1676 | $a->courseid = $attemptobj->get_course()->id; |
1c2e05c0 TH |
1677 | $a->coursename = format_string($attemptobj->get_course()->fullname); |
1678 | $a->courseshortname = format_string($attemptobj->get_course()->shortname); | |
fd0537ce TH |
1679 | // Quiz info. |
1680 | $a->quizname = $quizname; | |
1c2e05c0 | 1681 | $a->quizurl = $attemptobj->view_url(); |
fd0537ce TH |
1682 | $a->quizlink = '<a href="' . $a->quizurl . '">' . $quizname . '</a>'; |
1683 | // Attempt info. | |
1c2e05c0 | 1684 | $a->attemptduedate = userdate($duedate); |
59d14f31 | 1685 | $a->attemptgraceend = userdate($graceend); |
1c2e05c0 | 1686 | $a->attemptsummaryurl = $attemptobj->summary_url()->out(false); |
fd0537ce TH |
1687 | $a->attemptsummarylink = '<a href="' . $a->attemptsummaryurl . '">' . $quizname . ' review</a>'; |
1688 | // Student's info. | |
1689 | $a->studentidnumber = $submitter->idnumber; | |
1690 | $a->studentname = fullname($submitter); | |
1691 | $a->studentusername = $submitter->username; | |
1692 | ||
1693 | // Prepare the message. | |
cc350fd9 AD |
1694 | $eventdata = new \core\message\message(); |
1695 | $eventdata->courseid = $a->courseid; | |
fd0537ce TH |
1696 | $eventdata->component = 'mod_quiz'; |
1697 | $eventdata->name = 'attempt_overdue'; | |
1698 | $eventdata->notification = 1; | |
1699 | ||
5540b2ca | 1700 | $eventdata->userfrom = core_user::get_noreply_user(); |
fd0537ce TH |
1701 | $eventdata->userto = $submitter; |
1702 | $eventdata->subject = get_string('emailoverduesubject', 'quiz', $a); | |
1703 | $eventdata->fullmessage = get_string('emailoverduebody', 'quiz', $a); | |
1704 | $eventdata->fullmessageformat = FORMAT_PLAIN; | |
1705 | $eventdata->fullmessagehtml = ''; | |
1706 | ||
1707 | $eventdata->smallmessage = get_string('emailoverduesmall', 'quiz', $a); | |
1708 | $eventdata->contexturl = $a->quizurl; | |
1709 | $eventdata->contexturlname = $a->quizname; | |
1710 | ||
1711 | // Send the message. | |
1712 | return message_send($eventdata); | |
1713 | } | |
1714 | ||
cbb8b55c TH |
1715 | /** |
1716 | * Handle the quiz_attempt_submitted event. | |
1717 | * | |
1718 | * This sends the confirmation and notification messages, if required. | |
1719 | * | |
1720 | * @param object $event the event object. | |
1721 | */ | |
1722 | function quiz_attempt_submitted_handler($event) { | |
1723 | global $DB; | |
1724 | ||
1725 | $course = $DB->get_record('course', array('id' => $event->courseid)); | |
d900f1dc FM |
1726 | $attempt = $event->get_record_snapshot('quiz_attempts', $event->objectid); |
1727 | $quiz = $event->get_record_snapshot('quiz', $attempt->quiz); | |
1728 | $cm = get_coursemodule_from_id('quiz', $event->get_context()->instanceid, $event->courseid); | |
cbb8b55c TH |
1729 | |
1730 | if (!($course && $quiz && $cm && $attempt)) { | |
1731 | // Something has been deleted since the event was raised. Therefore, the | |
1732 | // event is no longer relevant. | |
1733 | return true; | |
ac48e43a | 1734 | } |
ac48e43a | 1735 | |
db3686d5 RM |
1736 | // Update completion state. |
1737 | $completion = new completion_info($course); | |
1738 | if ($completion->is_enabled($cm) && ($quiz->completionattemptsexhausted || $quiz->completionpass)) { | |
1739 | $completion->update_state($cm, COMPLETION_COMPLETE, $event->userid); | |
1740 | } | |
cbb8b55c | 1741 | return quiz_send_notification_messages($course, $quiz, $attempt, |
c492a78e | 1742 | context_module::instance($cm->id), $cm); |
ac48e43a | 1743 | } |
fa583f5f | 1744 | |
8e771aed MP |
1745 | /** |
1746 | * Handle groups_member_added event | |
1747 | * | |
1748 | * @param object $event the event object. | |
11b0c8e0 | 1749 | * @deprecated since 2.6, see {@link \mod_quiz\group_observers::group_member_added()}. |
8e771aed MP |
1750 | */ |
1751 | function quiz_groups_member_added_handler($event) { | |
11b0c8e0 FM |
1752 | debugging('quiz_groups_member_added_handler() is deprecated, please use ' . |
1753 | '\mod_quiz\group_observers::group_member_added() instead.', DEBUG_DEVELOPER); | |
8e771aed MP |
1754 | quiz_update_open_attempts(array('userid'=>$event->userid, 'groupid'=>$event->groupid)); |
1755 | } | |
1756 | ||
1757 | /** | |
1758 | * Handle groups_member_removed event | |
1759 | * | |
1760 | * @param object $event the event object. | |
11b0c8e0 | 1761 | * @deprecated since 2.6, see {@link \mod_quiz\group_observers::group_member_removed()}. |
8e771aed MP |
1762 | */ |
1763 | function quiz_groups_member_removed_handler($event) { | |
11b0c8e0 FM |
1764 | debugging('quiz_groups_member_removed_handler() is deprecated, please use ' . |
1765 | '\mod_quiz\group_observers::group_member_removed() instead.', DEBUG_DEVELOPER); | |
8e771aed MP |
1766 | quiz_update_open_attempts(array('userid'=>$event->userid, 'groupid'=>$event->groupid)); |
1767 | } | |
1768 | ||
1769 | /** | |
1770 | * Handle groups_group_deleted event | |
1771 | * | |
1772 | * @param object $event the event object. | |
11b0c8e0 | 1773 | * @deprecated since 2.6, see {@link \mod_quiz\group_observers::group_deleted()}. |
8e771aed MP |
1774 | */ |
1775 | function quiz_groups_group_deleted_handler($event) { | |
1776 | global $DB; | |
11b0c8e0 FM |
1777 | debugging('quiz_groups_group_deleted_handler() is deprecated, please use ' . |
1778 | '\mod_quiz\group_observers::group_deleted() instead.', DEBUG_DEVELOPER); | |
1779 | quiz_process_group_deleted_in_course($event->courseid); | |
1780 | } | |
1781 | ||
1782 | /** | |
1783 | * Logic to happen when a/some group(s) has/have been deleted in a course. | |
1784 | * | |
1785 | * @param int $courseid The course ID. | |
1786 | * @return void | |
1787 | */ | |
1788 | function quiz_process_group_deleted_in_course($courseid) { | |
1789 | global $DB; | |
8e771aed MP |
1790 | |
1791 | // It would be nice if we got the groupid that was deleted. | |
11b0c8e0 | 1792 | // Instead, we just update all quizzes with orphaned group overrides. |
8e771aed MP |
1793 | $sql = "SELECT o.id, o.quiz |
1794 | FROM {quiz_overrides} o | |
1795 | JOIN {quiz} quiz ON quiz.id = o.quiz | |
1796 | LEFT JOIN {groups} grp ON grp.id = o.groupid | |
74b3c4e5 MP |
1797 | WHERE quiz.course = :courseid |
1798 | AND o.groupid IS NOT NULL | |
1799 | AND grp.id IS NULL"; | |
11b0c8e0 | 1800 | $params = array('courseid' => $courseid); |
8e771aed | 1801 | $records = $DB->get_records_sql_menu($sql, $params); |
1744033c TH |
1802 | if (!$records) { |
1803 | return; // Nothing to do. | |
1804 | } | |
8e771aed | 1805 | $DB->delete_records_list('quiz_overrides', 'id', array_keys($records)); |
11b0c8e0 | 1806 | quiz_update_open_attempts(array('quizid' => array_unique(array_values($records)))); |
8e771aed MP |
1807 | } |
1808 | ||
1809 | /** | |
1810 | * Handle groups_members_removed event | |
1811 | * | |
1812 | * @param object $event the event object. | |
11b0c8e0 | 1813 | * @deprecated since 2.6, see {@link \mod_quiz\group_observers::group_member_removed()}. |
8e771aed MP |
1814 | */ |
1815 | function quiz_groups_members_removed_handler($event) { | |
11b0c8e0 FM |
1816 | debugging('quiz_groups_members_removed_handler() is deprecated, please use ' . |
1817 | '\mod_quiz\group_observers::group_member_removed() instead.', DEBUG_DEVELOPER); | |
8e771aed MP |
1818 | if ($event->userid == 0) { |
1819 | quiz_update_open_attempts(array('courseid'=>$event->courseid)); | |
1820 | } else { | |
1821 | quiz_update_open_attempts(array('courseid'=>$event->courseid, 'userid'=>$event->userid)); | |
1822 | } | |
1823 | } | |
1824 | ||
fd0537ce TH |
1825 | /** |
1826 | * Get the information about the standard quiz JavaScript module. | |
1827 | * @return array a standard jsmodule structure. | |
1828 | */ | |
ff065f96 | 1829 | function quiz_get_js_module() { |
157434a5 | 1830 | global $PAGE; |
73a7a0c9 | 1831 | |
ff065f96 TH |
1832 | return array( |
1833 | 'name' => 'mod_quiz', | |
1834 | 'fullpath' => '/mod/quiz/module.js', | |
25a03faa | 1835 | 'requires' => array('base', 'dom', 'event-delegate', 'event-key', |
aebbfb7b | 1836 | 'core_question_engine', 'moodle-core-formchangechecker'), |
ff065f96 | 1837 | 'strings' => array( |
73a7a0c9 | 1838 | array('cancel', 'moodle'), |
a26246ea | 1839 | array('flagged', 'question'), |
d755b0f5 TH |
1840 | array('functiondisabledbysecuremode', 'quiz'), |
1841 | array('startattempt', 'quiz'), | |
1842 | array('timesup', 'quiz'), | |
aebbfb7b | 1843 | array('changesmadereallygoaway', 'moodle'), |
ff065f96 TH |
1844 | ), |
1845 | ); | |
fe6ce234 | 1846 | } |
25302dee TH |
1847 | |
1848 | ||
1849 | /** | |
1850 | * An extension of question_display_options that includes the extra options used | |
1851 | * by the quiz. | |
1852 | * | |
f7970e3c TH |
1853 | * @copyright 2010 The Open University |
1854 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
25302dee TH |
1855 | */ |
1856 | class mod_quiz_display_options extends question_display_options { | |
1857 | /**#@+ | |
1858 | * @var integer bits used to indicate various times in relation to a | |
1859 | * quiz attempt. | |
1860 | */ | |
1861 | const DURING = 0x10000; | |
1862 | const IMMEDIATELY_AFTER = 0x01000; | |
1863 | const LATER_WHILE_OPEN = 0x00100; | |
1864 | const AFTER_CLOSE = 0x00010; | |
1865 | /**#@-*/ | |
1866 | ||
1867 | /** | |
1868 | * @var boolean if this is false, then the student is not allowed to review | |
1869 | * anything about the attempt. | |
1870 | */ | |
1871 | public $attempt = true; | |
1872 | ||
1873 | /** | |
1874 | * @var boolean if this is false, then the student is not allowed to review | |
1875 | * anything about the attempt. | |
1876 | */ | |
1877 | public $overallfeedback = self::VISIBLE; | |
1878 | ||
1879 | /** | |
1880 | * Set up the various options from the quiz settings, and a time constant. | |
0ff4bd08 | 1881 | * @param object $quiz the quiz settings. |
f7970e3c | 1882 | * @param int $one of the {@link DURING}, {@link IMMEDIATELY_AFTER}, |
25302dee TH |
1883 | * {@link LATER_WHILE_OPEN} or {@link AFTER_CLOSE} constants. |
1884 | * @return mod_quiz_display_options set up appropriately. | |
1885 | */ | |
1886 | public static function make_from_quiz($quiz, $when) { | |
1887 | $options = new self(); | |
1888 | ||
1889 | $options->attempt = self::extract($quiz->reviewattempt, $when, true, false); | |
1890 | $options->correctness = self::extract($quiz->reviewcorrectness, $when); | |
25a03faa TH |
1891 | $options->marks = self::extract($quiz->reviewmarks, $when, |
1892 | self::MARK_AND_MAX, self::MAX_ONLY); | |
25302dee TH |
1893 | $options->feedback = self::extract($quiz->reviewspecificfeedback, $when); |
1894 | $options->generalfeedback = self::extract($quiz->reviewgeneralfeedback, $when); | |
1895 | $options->rightanswer = self::extract($quiz->reviewrightanswer, $when); | |
1896 | $options->overallfeedback = self::extract($quiz->reviewoverallfeedback, $when); | |
1897 | ||
1898 | $options->numpartscorrect = $options->feedback; | |
e101ec9f | 1899 | $options->manualcomment = $options->feedback; |
25302dee TH |
1900 | |
1901 | if ($quiz->questiondecimalpoints != -1) { | |
1902 | $options->markdp = $quiz->questiondecimalpoints; | |
1903 | } else { | |
1904 | $options->markdp = $quiz->decimalpoints; | |
1905 | } | |
1906 | ||
1907 | return $options; | |
1908 | } | |
1909 | ||
25a03faa TH |
1910 | protected static function extract($bitmask, $bit, |
1911 | $whenset = self::VISIBLE, $whennotset = self::HIDDEN) { | |
25302dee TH |
1912 | if ($bitmask & $bit) { |
1913 | return $whenset; | |
1914 | } else { | |
1915 | return $whennotset; | |
1916 | } | |
1917 | } | |
1918 | } | |
1919 | ||
1920 | ||
1921 | /** | |
1922 | * A {@link qubaid_condition} for finding all the question usages belonging to | |
1923 | * a particular quiz. | |
1924 | * | |
f7970e3c TH |
1925 | * @copyright 2010 The Open University |
1926 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
25302dee | 1927 | */ |
6b5f24d3 | 1928 | class qubaids_for_quiz extends qubaid_join { |
25302dee | 1929 | public function __construct($quizid, $includepreviews = true, $onlyfinished = false) { |
6b5f24d3 | 1930 | $where = 'quiza.quiz = :quizaquiz'; |
be18f589 TH |
1931 | $params = array('quizaquiz' => $quizid); |
1932 | ||
25302dee TH |
1933 | if (!$includepreviews) { |
1934 | $where .= ' AND preview = 0'; | |
1935 | } | |
be18f589 | 1936 | |
25302dee | 1937 | if ($onlyfinished) { |
81c53579 | 1938 | $where .= ' AND state = :statefinished'; |
be18f589 | 1939 | $params['statefinished'] = quiz_attempt::FINISHED; |
25302dee TH |
1940 | } |
1941 | ||
be18f589 | 1942 | parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params); |
25302dee TH |
1943 | } |
1944 | } | |
e1a2d0d9 CC |
1945 | |
1946 | /** | |
1947 | * Creates a textual representation of a question for display. | |
1948 | * | |
1949 | * @param object $question A question object from the database questions table | |
1950 | * @param bool $showicon If true, show the question's icon with the question. False by default. | |
1951 | * @param bool $showquestiontext If true (default), show question text after question name. | |
1952 | * If false, show only question name. | |
1953 | * @return string | |
1954 | */ | |
1955 | function quiz_question_tostring($question, $showicon = false, $showquestiontext = true) { | |
1956 | $result = ''; | |
1957 | ||
1958 | $name = shorten_text(format_string($question->name), 200); | |
1959 | if ($showicon) { | |
1960 | $name .= print_question_icon($question) . ' ' . $name; | |
1961 | } | |
1962 | $result .= html_writer::span($name, 'questionname'); | |
1963 | ||
1964 | if ($showquestiontext) { | |
1965 | $questiontext = question_utils::to_plain_text($question->questiontext, | |
1966 | $question->questiontextformat, array('noclean' => true, 'para' => false)); | |
1967 | $questiontext = shorten_text($questiontext, 200); | |
1968 | if ($questiontext) { | |
1969 | $result .= ' ' . html_writer::span(s($questiontext), 'questiontext'); | |
1970 | } | |
1971 | } | |
1972 | ||
1973 | return $result; | |
1974 | } | |
16fa272e TH |
1975 | |
1976 | /** | |
1977 | * Verify that the question exists, and the user has permission to use it. | |
1978 | * Does not return. Throws an exception if the question cannot be used. | |
1979 | * @param int $questionid The id of the question. | |
1980 | */ | |
1981 | function quiz_require_question_use($questionid) { | |
1982 | global $DB; | |
1983 | $question = $DB->get_record('question', array('id' => $questionid), '*', MUST_EXIST); | |
1984 | question_require_capability_on($question, 'use'); | |
1985 | } | |
1986 | ||
1987 | /** | |
1988 | * Verify that the question exists, and the user has permission to use it. | |
1989 | * @param object $quiz the quiz settings. | |
1990 | * @param int $slot which question in the quiz to test. | |
1991 | * @return bool whether the user can use this question. | |
1992 | */ | |
1993 | function quiz_has_question_use($quiz, $slot) { | |
1994 | global $DB; | |
1995 | $question = $DB->get_record_sql(" | |
1996 | SELECT q.* | |
1997 | FROM {quiz_slots} slot | |
1998 | JOIN {question} q ON q.id = slot.questionid | |
1999 | WHERE slot.quizid = ? AND slot.slot = ?", array($quiz->id, $slot)); | |
2000 | if (!$question) { | |
2001 | return false; | |
2002 | } | |
2003 | return question_has_capability_on($question, 'use'); | |
2004 | } | |
2005 | ||
2006 | /** | |
2007 | * Add a question to a quiz | |
2008 | * | |
2009 | * Adds a question to a quiz by updating $quiz as well as the | |
2010 | * quiz and quiz_slots tables. It also adds a page break if required. | |
2011 | * @param int $questionid The id of the question to be added | |
2012 | * @param object $quiz The extended quiz object as used by edit.php | |
2013 | * This is updated by this function | |
2014 | * @param int $page Which page in quiz to add the question on. If 0 (default), | |
2015 | * add at the end | |
2016 | * @param float $maxmark The maximum mark to set for this question. (Optional, | |
2017 | * defaults to question.defaultmark. | |
2018 | * @return bool false if the question was already in the quiz | |
2019 | */ | |
2020 | function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null) { | |
2021 | global $DB; | |
2022 | $slots = $DB->get_records('quiz_slots', array('quizid' => $quiz->id), | |
2023 | 'slot', 'questionid, slot, page, id'); | |
2024 | if (array_key_exists($questionid, $slots)) { | |
2025 | return false; | |
2026 | } | |
2027 | ||
2028 | $trans = $DB->start_delegated_transaction(); | |
2029 | ||
2030 | $maxpage = 1; | |
2031 | $numonlastpage = 0; | |
2032 | foreach ($slots as $slot) { | |
2033 | if ($slot->page > $maxpage) { | |
2034 | $maxpage = $slot->page; | |
2035 | $numonlastpage = 1; | |
2036 | } else { | |
2037 | $numonlastpage += 1; | |
2038 | } | |
2039 | } | |
2040 | ||
2041 | // Add the new question instance. | |
2042 | $slot = new stdClass(); | |
2043 | $slot->quizid = $quiz->id; | |
2044 | $slot->questionid = $questionid; | |
2045 | ||
2046 | if ($maxmark !== null) { | |
2047 | $slot->maxmark = $maxmark; | |
2048 | } else { | |
2049 | $slot->maxmark = $DB->get_field('question', 'defaultmark', array('id' => $questionid)); | |
2050 | } | |
2051 | ||
2052 | if (is_int($page) && $page >= 1) { | |
2053 | // Adding on a given page. | |
2054 | $lastslotbefore = 0; | |
2055 | foreach (array_reverse($slots) as $otherslot) { | |
2056 | if ($otherslot->page > $page) { | |
2057 | $DB->set_field('quiz_slots', 'slot', $otherslot->slot + 1, array('id' => $otherslot->id)); | |
2058 | } else { | |
2059 | $lastslotbefore = $otherslot->slot; | |
2060 | break; | |
2061 | } | |
2062 | } | |
2063 | $slot->slot = $lastslotbefore + 1; | |
2064 | $slot->page = min($page, $maxpage + 1); | |
2065 | ||
6c73dd95 | 2066 | quiz_update_section_firstslots($quiz->id, 1, max($lastslotbefore, 1)); |
5d949702 | 2067 | |
16fa272e TH |
2068 | } else { |
2069 | $lastslot = end($slots); | |
2070 | if ($lastslot) { | |
2071 | $slot->slot = $lastslot->slot + 1; | |
2072 | } else { | |
2073 | $slot->slot = 1; | |
2074 | } | |
2075 | if ($quiz->questionsperpage && $numonlastpage >= $quiz->questionsperpage) { | |
2076 | $slot->page = $maxpage + 1; | |
2077 | } else { | |
2078 | $slot->page = $maxpage; | |
2079 | } | |
2080 | } | |
2081 | ||
2082 | $DB->insert_record('quiz_slots', $slot); | |
2083 | $trans->allow_commit(); | |
2084 | } | |
2085 | ||
6c73dd95 TH |
2086 | /** |
2087 | * Move all the section headings in a certain slot range by a certain offset. | |
2088 | * | |
2089 | * @param int $quizid the id of a quiz | |
2090 | * @param int $direction amount to adjust section heading positions. Normally +1 or -1. | |
2091 | * @param int $afterslot adjust headings that start after this slot. | |
2092 | * @param int|null $beforeslot optionally, only adjust headings before this slot. | |
2093 | */ | |
2094 | function quiz_update_section_firstslots($quizid, $direction, $afterslot, $beforeslot = null) { | |
2095 | global $DB; | |
2096 | $where = 'quizid = ? AND firstslot > ?'; | |
2097 | $params = [$direction, $quizid, $afterslot]; | |
2098 | if ($beforeslot) { | |
2099 | $where .= ' AND firstslot < ?'; | |
2100 | $params[] = $beforeslot; | |
2101 | } | |
2102 | $firstslotschanges = $DB->get_records_select_menu('quiz_sections', | |
2103 | $where, $params, '', 'firstslot, firstslot + ?'); | |
2104 | update_field_with_unique_index('quiz_sections', 'firstslot', $firstslotschanges, ['quizid' => $quizid]); | |
2105 | } | |
2106 | ||
16fa272e TH |
2107 | /** |
2108 | * Add a random question to the quiz at a given point. | |
2109 | * @param object $quiz the quiz settings. | |
2110 | * @param int $addonpage the page on which to add the question. | |
2111 | * @param int $categoryid the question category to add the question from. | |
2112 | * @param int $number the number of random questions to add. | |
2113 | * @param bool $includesubcategories whether to include questoins from subcategories. | |
2114 | */ | |
2115 | function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number, | |
2116 | $includesubcategories) { | |
2117 | global $DB; | |
2118 | ||
2119 | $category = $DB->get_record('question_categories', array('id' => $categoryid)); | |
2120 | if (!$category) { | |
2121 | print_error('invalidcategoryid', 'error'); | |
2122 | } | |
2123 | ||
2124 | $catcontext = context::instance_by_id($category->contextid); | |
2125 | require_capability('moodle/question:useall', $catcontext); | |
2126 | ||
2127 | // Find existing random questions in this category that are | |
2128 | // not used by any quiz. | |
2129 | if ($existingquestions = $DB->get_records_sql( | |
2130 | "SELECT q.id, q.qtype FROM {question} q | |
2131 | WHERE qtype = 'random' | |
2132 | AND category = ? | |
2133 | AND " . $DB->sql_compare_text('questiontext') . " = ? | |
2134 | AND NOT EXISTS ( | |
2135 | SELECT * | |
2136 | FROM {quiz_slots} | |
2137 | WHERE questionid = q.id) | |
2138 | ORDER BY id", array($category->id, ($includesubcategories ? '1' : '0')))) { | |
2139 | // Take as many of these as needed. | |
2140 | while (($existingquestion = array_shift($existingquestions)) && $number > 0) { | |
2141 | quiz_add_quiz_question($existingquestion->id, $quiz, $addonpage); | |
2142 | $number -= 1; | |
2143 | } | |
2144 | } | |
2145 | ||
2146 | if ($number <= 0) { | |
2147 | return; | |
2148 | } | |
2149 | ||
2150 | // More random questions are needed, create them. | |
2151 | for ($i = 0; $i < $number; $i += 1) { | |
2152 | $form = new stdClass(); | |
2153 | $form->questiontext = array('text' => ($includesubcategories ? '1' : '0'), 'format' => 0); | |
2154 | $form->category = $category->id . ',' . $category->contextid; | |
2155 | $form->defaultmark = 1; | |
2156 | $form->hidden = 1; | |
2157 | $form->stamp = make_unique_id_code(); // Set the unique code (not to be changed). | |
2158 | $question = new stdClass(); | |
2159 | $question->qtype = 'random'; | |
2160 | $question = question_bank::get_qtype('random')->save_question($question, $form); | |
2161 | if (!isset($question->id)) { | |
2162 | print_error('cannotinsertrandomquestion', 'quiz'); | |
2163 | } | |
2164 | quiz_add_quiz_question($question->id, $quiz, $addonpage); | |
2165 | } | |
2166 | } | |
19429de4 JL |
2167 | |
2168 | /** | |
2169 | * Mark the activity completed (if required) and trigger the course_module_viewed event. | |
2170 | * | |
2171 | * @param stdClass $quiz quiz object | |
2172 | * @param stdClass $course course object | |
2173 | * @param stdClass $cm course module object | |
2174 | * @param stdClass $context context object | |
2175 | * @since Moodle 3.1 | |
2176 | */ | |
2177 | function quiz_view($quiz, $course, $cm, $context) { | |
2178 | ||
2179 | $params = array( | |
2180 | 'objectid' => $quiz->id, | |
2181 | 'context' => $context | |
2182 | ); | |
2183 | ||
2184 | $event = \mod_quiz\event\course_module_viewed::create($params); | |
2185 | $event->add_record_snapshot('quiz', $quiz); | |
2186 | $event->trigger(); | |
2187 | ||
2188 | // Completion. | |
2189 | $completion = new completion_info($course); | |
2190 | $completion->set_module_viewed($cm); | |
0057c2ce JL |
2191 | } |
2192 | ||
2193 | /** | |
2194 | * Validate permissions for creating a new attempt and start a new preview attempt if required. | |
2195 | * | |
2196 | * @param quiz $quizobj quiz object | |
2197 | * @param quiz_access_manager $accessmanager quiz access manager | |
2198 | * @param bool $forcenew whether was required to start a new preview attempt | |
2199 | * @param int $page page to jump to in the attempt | |
2200 | * @param bool $redirect whether to redirect or throw exceptions (for web or ws usage) | |
2201 | * @return array an array containing the attempt information, access error messages and the page to jump to in the attempt | |
2202 | * @throws moodle_quiz_exception | |
2203 | * @since Moodle 3.1 | |
2204 | */ | |
2205 | function quiz_validate_new_attempt(quiz $quizobj, quiz_access_manager $accessmanager, $forcenew, $page, $redirect) { | |
2206 | global $DB, $USER; | |
2207 | $timenow = time(); | |
2208 | ||
2209 | if ($quizobj->is_preview_user() && $forcenew) { | |
2210 | $accessmanager->current_attempt_finished(); | |
2211 | } | |
2212 | ||
2213 | // Check capabilities. | |
2214 | if (!$quizobj->is_preview_user()) { | |
2215 | $quizobj->require_capability('mod/quiz:attempt'); | |
2216 | } | |
2217 | ||
2218 | // Check to see if a new preview was requested. | |
2219 | if ($quizobj->is_preview_user() && $forcenew) { | |
2220 | // To force the creation of a new preview, we mark the current attempt (if any) | |
2221 | // as finished. It will then automatically be deleted below. | |
2222 | $DB->set_field('quiz_attempts', 'state', quiz_attempt::FINISHED, | |
2223 | array('quiz' => $quizobj->get_quizid(), 'userid' => $USER->id)); | |
2224 | } | |
2225 | ||
2226 | // Look for an existing attempt. | |
2227 | $attempts = quiz_get_user_attempts($quizobj->get_quizid(), $USER->id, 'all', true); | |
2228 | $lastattempt = end($attempts); | |
2229 | ||
2230 | $attemptnumber = null; | |
2231 | // If an in-progress attempt exists, check password then redirect to it. | |
2232 | if ($lastattempt && ($lastattempt->state == quiz_attempt::IN_PROGRESS || | |
2233 | $lastattempt->state == quiz_attempt::OVERDUE)) { | |
2234 | $currentattemptid = $lastattempt->id; | |
2235 | $messages = $accessmanager->prevent_access(); | |
2236 | ||
2237 | // If the attempt is now overdue, deal with that. | |
2238 | $quizobj->create_attempt_object($lastattempt)->handle_if_time_expired($timenow, true); | |
2239 | ||
2240 | // And, if the attempt is now no longer in progress, redirect to the appropriate place. | |
2241 | if ($lastattempt->state == quiz_attempt::ABANDONED || $lastattempt->state == quiz_attempt::FINISHED) { | |
2242 | if ($redirect) { | |
2243 | redirect($quizobj->review_url($lastattempt->id)); | |
2244 | } else { | |
2245 | throw new moodle_quiz_exception($quizobj, 'attemptalreadyclosed'); | |
2246 | } | |
2247 | } | |
2248 | ||
2249 | // If the page number was not explicitly in the URL, go to the current page. | |
2250 | if ($page == -1) { | |
2251 | $page = $lastattempt->currentpage; | |
2252 | } | |
2253 | ||
2254 | } else { | |
2255 | while ($lastattempt && $lastattempt->preview) { | |
2256 | $lastattempt = array_pop($attempts); | |
2257 | } | |
19429de4 | 2258 | |
0057c2ce JL |
2259 | // Get number for the next or unfinished attempt. |
2260 | if ($lastattempt) { | |
2261 | $attemptnumber = $lastattempt->attempt + 1; | |
2262 | } else { | |
2263 | $lastattempt = false; | |
2264 | $attemptnumber = 1; | |
2265 | } | |
2266 | $currentattemptid = null; | |
2267 | ||
2268 | $messages = $accessmanager->prevent_access() + | |
2269 | $accessmanager->prevent_new_attempt(count($attempts), $lastattempt); | |
2270 | ||
2271 | if ($page == -1) { | |
2272 | $page = 0; | |
2273 | } | |
2274 | } | |
2275 | return array($currentattemptid, $attemptnumber, $lastattempt, $messages, $page); | |
2276 | } | |
2277 | ||
2278 | /** | |
2279 | * Prepare and start a new attempt deleting the previous preview attempts. | |
2280 | * | |
2281 | * @param quiz $quizobj quiz object | |
2282 | * @param int $attemptnumber the attempt number | |
2283 | * @param object $lastattempt last attempt object | |
8a972ab3 | 2284 | * @param bool $offlineattempt whether is an offline attempt or not |
0057c2ce JL |
2285 | * @return object the new attempt |
2286 | * @since Moodle 3.1 | |
2287 | */ | |
8a972ab3 | 2288 | function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $offlineattempt = false) { |
0057c2ce JL |
2289 | global $DB, $USER; |
2290 | ||
2291 | // Delete any previous preview attempts belonging to this user. | |
2292 | quiz_delete_previews($quizobj->get_quiz(), $USER->id); | |
2293 | ||
2294 | $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); | |
2295 | $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); | |
2296 | ||
2297 | // Create the new attempt and initialize the question sessions | |
2298 | $timenow = time(); // Update time now, in case the server is running really slowly. | |
2299 | $attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow, $quizobj->is_preview_user()); | |
2300 | ||
2301 | if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) { | |
2302 | $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow); | |
2303 | } else { | |
2304 | $attempt = quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt); | |
2305 | } | |
2306 | ||
2307 | $transaction = $DB->start_delegated_transaction(); | |
2308 | ||
8a972ab3 JL |
2309 | // Init the timemodifiedoffline for offline attempts. |
2310 | if ($offlineattempt) { | |
2311 | $attempt->timemodifiedoffline = $attempt->timemodified; | |
2312 | } | |
0057c2ce JL |
2313 | $attempt = quiz_attempt_save_started($quizobj, $quba, $attempt); |
2314 | ||
2315 | $transaction->allow_commit(); | |
2316 | ||
2317 | return $attempt; | |
19429de4 | 2318 | } |
9aac9f70 RW |
2319 | |
2320 | /** | |
2321 | * Check if the given calendar_event is either a user or group override | |
2322 | * event for quiz. | |
2323 | * | |
2324 | * @param calendar_event $event The calendar event to check | |
2325 | * @return bool | |
2326 | */ | |
2327 | function quiz_is_overriden_calendar_event(\calendar_event $event) { | |
2328 | global $DB; | |
2329 | ||
2330 | if (!isset($event->modulename)) { | |
2331 | return false; | |
2332 | } | |
2333 | ||
2334 | if ($event->modulename != 'quiz') { | |
2335 | return false; | |
2336 | } | |
2337 | ||
2338 | if (!isset($event->instance)) { | |
2339 | return false; | |
2340 | } | |
2341 | ||
2342 | if (!isset($event->userid) && !isset($event->groupid)) { | |
2343 | return false; | |
2344 | } | |
2345 | ||
2346 | $overrideparams = [ | |
2347 | 'quiz' => $event->instance | |
2348 | ]; | |
2349 | ||
2350 | if (isset($event->groupid)) { | |
2351 | $overrideparams['groupid'] = $event->groupid; | |
2352 | } else if (isset($event->userid)) { | |
2353 | $overrideparams['userid'] = $event->userid; | |
2354 | } | |
2355 | ||
2356 | return $DB->record_exists('quiz_overrides', $overrideparams); | |
2357 | } |