MDL-55188 events: First deprecation of eventslib.php
[moodle.git] / mod / quiz / lib.php
CommitLineData
8cc86111 1<?php
8cc86111 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/**
84e628a0 18 * Library of functions for the quiz module.
19 *
20 * This contains functions that are called also from outside the quiz module
21 * Functions that are only called by the quiz module itself are in {@link locallib.php}
22 *
01773a6d 23 * @package mod_quiz
ba643847
TH
24 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
84e628a0 26 */
730fd187 27
25302dee 28
a17b297d
TH
29defined('MOODLE_INTERNAL') || die();
30
f81a8247 31require_once($CFG->dirroot . '/calendar/lib.php');
8966a111 32
75cd257b 33
e2249afe 34/**#@+
25302dee 35 * Option controlling what options are offered on the quiz settings form.
e2249afe 36 */
84e628a0 37define('QUIZ_MAX_ATTEMPT_OPTION', 10);
38define('QUIZ_MAX_QPP_OPTION', 50);
39define('QUIZ_MAX_DECIMAL_OPTION', 5);
40define('QUIZ_MAX_Q_DECIMAL_OPTION', 7);
75cd257b 41/**#@-*/
42
4344c5d5
TH
43/**#@+
44 * Options determining how the grades from individual attempts are combined to give
45 * the overall grade for a user
46 */
47define('QUIZ_GRADEHIGHEST', '1');
48define('QUIZ_GRADEAVERAGE', '2');
49define('QUIZ_ATTEMPTFIRST', '3');
50define('QUIZ_ATTEMPTLAST', '4');
51/**#@-*/
52
75cd257b 53/**
9e83f3d1 54 * @var int If start and end date for the quiz are more than this many seconds apart
75cd257b 55 * they will be represented by two separate events in the calendar
56 */
9e83f3d1 57define('QUIZ_MAX_EVENT_LENGTH', 5*24*60*60); // 5 days.
ee1fb969 58
33c8d37b
CF
59/**#@+
60 * Options for navigation method within quizzes.
61 */
62define('QUIZ_NAVMETHOD_FREE', 'free');
63define('QUIZ_NAVMETHOD_SEQ', 'sequential');
64/**#@-*/
65
b3d7d175
MN
66/**
67 * Event types.
68 */
69define('QUIZ_EVENT_TYPE_OPEN', 'open');
70define('QUIZ_EVENT_TYPE_CLOSE', 'close');
71
920b93d1 72/**
73 * Given an object containing all the necessary data,
7cac0c4b 74 * (defined by the form in mod_form.php) this function
920b93d1 75 * will create a new instance and return the id number
76 * of the new instance.
a23f0aaf 77 *
920b93d1 78 * @param object $quiz the data that came from the form.
212b7b8c 79 * @return mixed the id of the new instance on success,
80 * false or a string error message on failure.
920b93d1 81 */
730fd187 82function quiz_add_instance($quiz) {
c18269c7 83 global $DB;
fe6ce234 84 $cmid = $quiz->coursemodule;
730fd187 85
920b93d1 86 // Process the options from the form.
87 $quiz->created = time();
212b7b8c 88 $result = quiz_process_options($quiz);
89 if ($result && is_string($result)) {
90 return $result;
91 }
6f797013 92
920b93d1 93 // Try to store it in the database.
eeab18f0 94 $quiz->id = $DB->insert_record('quiz', $quiz);
7bd1aa1d 95
5d949702
K
96 // Create the first section for this quiz.
97 $DB->insert_record('quiz_sections', array('quizid' => $quiz->id,
98 'firstslot' => 1, 'heading' => '', 'shufflequestions' => 0));
99
920b93d1 100 // Do the processing required after an add or an update.
101 quiz_after_add_or_update($quiz);
a23f0aaf 102
7bd1aa1d 103 return $quiz->id;
730fd187 104}
105
920b93d1 106/**
107 * Given an object containing all the necessary data,
7cac0c4b 108 * (defined by the form in mod_form.php) this function
920b93d1 109 * will update an existing instance with new data.
a23f0aaf 110 *
920b93d1 111 * @param object $quiz the data that came from the form.
212b7b8c 112 * @return mixed true on success, false or a string error message on failure.
920b93d1 113 */
eeab18f0 114function quiz_update_instance($quiz, $mform) {
115 global $CFG, $DB;
475225a1 116 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
730fd187 117
920b93d1 118 // Process the options from the form.
212b7b8c 119 $result = quiz_process_options($quiz);
120 if ($result && is_string($result)) {
121 return $result;
122 }
ee1fb969 123
475225a1 124 // Get the current value, so we can see what changed.
f2557823 125 $oldquiz = $DB->get_record('quiz', array('id' => $quiz->instance));
25302dee 126
475225a1
TH
127 // We need two values from the existing DB record that are not in the form,
128 // in some of the function calls below.
129 $quiz->sumgrades = $oldquiz->sumgrades;
130 $quiz->grade = $oldquiz->grade;
131
920b93d1 132 // Update the database.
730fd187 133 $quiz->id = $quiz->instance;
eeab18f0 134 $DB->update_record('quiz', $quiz);
730fd187 135
920b93d1 136 // Do the processing required after an add or an update.
137 quiz_after_add_or_update($quiz);
ee1fb969 138
25302dee 139 if ($oldquiz->grademethod != $quiz->grademethod) {
25302dee
TH
140 quiz_update_all_final_grades($quiz);
141 quiz_update_grades($quiz);
142 }
143
475225a1
TH
144 $quizdateschanged = $oldquiz->timelimit != $quiz->timelimit
145 || $oldquiz->timeclose != $quiz->timeclose
146 || $oldquiz->graceperiod != $quiz->graceperiod;
147 if ($quizdateschanged) {
148 quiz_update_open_attempts(array('quizid' => $quiz->id));
8e771aed
MP
149 }
150
9e83f3d1 151 // Delete any previous preview attempts.
53004e48 152 quiz_delete_previews($quiz);
d2f308c0 153
ccba5b88 154 // Repaginate, if asked to.
5d949702 155 if (!empty($quiz->repaginatenow)) {
ccba5b88
TH
156 quiz_repaginate_questions($quiz->id, $quiz->questionsperpage);
157 }
158
7bd1aa1d 159 return true;
730fd187 160}
161
8cc86111 162/**
163 * Given an ID of an instance of this module,
164 * this function will permanently delete the instance
165 * and any data that depends on it.
166 *
25302dee 167 * @param int $id the id of the quiz to delete.
f7970e3c 168 * @return bool success or failure.
8cc86111 169 */
730fd187 170function quiz_delete_instance($id) {
c18269c7 171 global $DB;
730fd187 172
25302dee 173 $quiz = $DB->get_record('quiz', array('id' => $id), '*', MUST_EXIST);
730fd187 174
53004e48 175 quiz_delete_all_attempts($quiz);
990650f9 176 quiz_delete_all_overrides($quiz);
730fd187 177
828a4e9f
EM
178 // Look for random questions that may no longer be used when this quiz is gone.
179 $sql = "SELECT q.id
180 FROM {quiz_slots} slot
181 JOIN {question} q ON q.id = slot.questionid
182 WHERE slot.quizid = ? AND q.qtype = ?";
183 $questionids = $DB->get_fieldset_sql($sql, array($quiz->id, 'random'));
184
3090719d
SR
185 // We need to do the following deletes before we try and delete randoms, otherwise they would still be 'in use'.
186 $quizslots = $DB->get_fieldset_select('quiz_slots', 'id', 'quizid = ?', array($quiz->id));
187 $DB->delete_records_list('quiz_slot_tags', 'slotid', $quizslots);
ccba5b88 188 $DB->delete_records('quiz_slots', array('quizid' => $quiz->id));
5d949702 189 $DB->delete_records('quiz_sections', array('quizid' => $quiz->id));
828a4e9f
EM
190
191 foreach ($questionids as $questionid) {
192 question_delete_question($questionid);
193 }
194
53004e48 195 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
730fd187 196
35cbe84b
TH
197 quiz_access_manager::delete_settings($quiz);
198
53004e48 199 $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id));
25a03faa 200 foreach ($events as $event) {
f81a8247
SH
201 $event = calendar_event::load($event);
202 $event->delete();
b2a3cd2d 203 }
204
d6dd2108 205 quiz_grade_item_delete($quiz);
53004e48 206 $DB->delete_records('quiz', array('id' => $quiz->id));
d6dd2108 207
53004e48 208 return true;
209}
210
990650f9
TH
211/**
212 * Deletes a quiz override from the database and clears any corresponding calendar events
213 *
214 * @param object $quiz The quiz object.
f7970e3c 215 * @param int $overrideid The id of the override being deleted
4c729982 216 * @param bool $log Whether to trigger logs.
990650f9
TH
217 * @return bool true on success
218 */
4c729982 219function quiz_delete_override($quiz, $overrideid, $log = true) {
990650f9
TH
220 global $DB;
221
92089e0e
MN
222 if (!isset($quiz->cmid)) {
223 $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
224 $quiz->cmid = $cm->id;
225 }
226
25302dee 227 $override = $DB->get_record('quiz_overrides', array('id' => $overrideid), '*', MUST_EXIST);
990650f9 228
9e83f3d1 229 // Delete the events.
99618f89
LB
230 if (isset($override->groupid)) {
231 // Create the search array for a group override.
232 $eventsearcharray = array('modulename' => 'quiz',
233 'instance' => $quiz->id, 'groupid' => (int)$override->groupid);
234 } else {
235 // Create the search array for a user override.
236 $eventsearcharray = array('modulename' => 'quiz',
237 'instance' => $quiz->id, 'userid' => (int)$override->userid);
238 }
239 $events = $DB->get_records('event', $eventsearcharray);
25302dee 240 foreach ($events as $event) {
990650f9
TH
241 $eventold = calendar_event::load($event);
242 $eventold->delete();
243 }
244
245 $DB->delete_records('quiz_overrides', array('id' => $overrideid));
8de2eb35 246
4c729982
AN
247 if ($log) {
248 // Set the common parameters for one of the events we will be triggering.
249 $params = array(
250 'objectid' => $override->id,
251 'context' => context_module::instance($quiz->cmid),
252 'other' => array(
253 'quizid' => $override->quiz
254 )
255 );
256 // Determine which override deleted event to fire.
257 if (!empty($override->userid)) {
258 $params['relateduserid'] = $override->userid;
259 $event = \mod_quiz\event\user_override_deleted::create($params);
260 } else {
261 $params['other']['groupid'] = $override->groupid;
262 $event = \mod_quiz\event\group_override_deleted::create($params);
263 }
8de2eb35 264
4c729982
AN
265 // Trigger the override deleted event.
266 $event->add_record_snapshot('quiz_overrides', $override);
267 $event->trigger();
268 }
8de2eb35 269
990650f9
TH
270 return true;
271}
272
273/**
274 * Deletes all quiz overrides from the database and clears any corresponding calendar events
275 *
276 * @param object $quiz The quiz object.
4c729982 277 * @param bool $log Whether to trigger logs.
990650f9 278 */
4c729982 279function quiz_delete_all_overrides($quiz, $log = true) {
990650f9
TH
280 global $DB;
281
282 $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id');
283 foreach ($overrides as $override) {
4c729982 284 quiz_delete_override($quiz, $override->id, $log);
990650f9
TH
285 }
286}
287
288/**
289 * Updates a quiz object with override information for a user.
290 *
291 * Algorithm: For each quiz setting, if there is a matching user-specific override,
292 * then use that otherwise, if there are group-specific overrides, return the most
293 * lenient combination of them. If neither applies, leave the quiz setting unchanged.
294 *
295 * Special case: if there is more than one password that applies to the user, then
296 * quiz->extrapasswords will contain an array of strings giving the remaining
297 * passwords.
298 *
299 * @param object $quiz The quiz object.
f7970e3c 300 * @param int $userid The userid.
990650f9
TH
301 * @return object $quiz The updated quiz object.
302 */
303function quiz_update_effective_access($quiz, $userid) {
304 global $DB;
305
9e83f3d1 306 // Check for user override.
990650f9
TH
307 $override = $DB->get_record('quiz_overrides', array('quiz' => $quiz->id, 'userid' => $userid));
308
309 if (!$override) {
6bdfef5d 310 $override = new stdClass();
990650f9
TH
311 $override->timeopen = null;
312 $override->timeclose = null;
313 $override->timelimit = null;
314 $override->attempts = null;
315 $override->password = null;
316 }
317
9e83f3d1 318 // Check for group overrides.
990650f9 319 $groupings = groups_get_user_groups($quiz->course, $userid);
990650f9 320
7bc488dc 321 if (!empty($groupings[0])) {
9e83f3d1 322 // Select all overrides that apply to the User's groups.
7bc488dc 323 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
990650f9
TH
324 $sql = "SELECT * FROM {quiz_overrides}
325 WHERE groupid $extra AND quiz = ?";
ac250ad5 326 $params[] = $quiz->id;
990650f9
TH
327 $records = $DB->get_records_sql($sql, $params);
328
9e83f3d1 329 // Combine the overrides.
990650f9
TH
330 $opens = array();
331 $closes = array();
332 $limits = array();
333 $attempts = array();
334 $passwords = array();
335
336 foreach ($records as $gpoverride) {
337 if (isset($gpoverride->timeopen)) {
338 $opens[] = $gpoverride->timeopen;
339 }
340 if (isset($gpoverride->timeclose)) {
341 $closes[] = $gpoverride->timeclose;
342 }
343 if (isset($gpoverride->timelimit)) {
344 $limits[] = $gpoverride->timelimit;
345 }
346 if (isset($gpoverride->attempts)) {
347 $attempts[] = $gpoverride->attempts;
348 }
349 if (isset($gpoverride->password)) {
350 $passwords[] = $gpoverride->password;
351 }
352 }
9e83f3d1 353 // If there is a user override for a setting, ignore the group override.
990650f9 354 if (is_null($override->timeopen) && count($opens)) {
25302dee 355 $override->timeopen = min($opens);
990650f9
TH
356 }
357 if (is_null($override->timeclose) && count($closes)) {
8e771aed
MP
358 if (in_array(0, $closes)) {
359 $override->timeclose = 0;
360 } else {
361 $override->timeclose = max($closes);
362 }
990650f9
TH
363 }
364 if (is_null($override->timelimit) && count($limits)) {
8e771aed
MP
365 if (in_array(0, $limits)) {
366 $override->timelimit = 0;
367 } else {
368 $override->timelimit = max($limits);
369 }
990650f9
TH
370 }
371 if (is_null($override->attempts) && count($attempts)) {
8e771aed
MP
372 if (in_array(0, $attempts)) {
373 $override->attempts = 0;
374 } else {
375 $override->attempts = max($attempts);
376 }
990650f9
TH
377 }
378 if (is_null($override->password) && count($passwords)) {
25302dee 379 $override->password = array_shift($passwords);
990650f9 380 if (count($passwords)) {
25302dee 381 $override->extrapasswords = $passwords;
990650f9
TH
382 }
383 }
384
385 }
386
9e83f3d1 387 // Merge with quiz defaults.
25302dee 388 $keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password', 'extrapasswords');
990650f9
TH
389 foreach ($keys as $key) {
390 if (isset($override->{$key})) {
391 $quiz->{$key} = $override->{$key};
392 }
393 }
394
395 return $quiz;
396}
397
53004e48 398/**
399 * Delete all the attempts belonging to a quiz.
8cc86111 400 *
8cc86111 401 * @param object $quiz The quiz object.
53004e48 402 */
403function quiz_delete_all_attempts($quiz) {
404 global $CFG, $DB;
6b5f24d3
TH
405 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
406 question_engine::delete_questions_usage_by_activities(new qubaids_for_quiz($quiz->id));
53004e48 407 $DB->delete_records('quiz_attempts', array('quiz' => $quiz->id));
408 $DB->delete_records('quiz_grades', array('quiz' => $quiz->id));
730fd187 409}
410
4c729982
AN
411/**
412 * Delete all the attempts belonging to a user in a particular quiz.
413 *
414 * @param object $quiz The quiz object.
415 * @param object $user The user object.
416 */
417function quiz_delete_user_attempts($quiz, $user) {
418 global $CFG, $DB;
419 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
420 question_engine::delete_questions_usage_by_activities(new qubaids_for_quiz_user($quiz->get_quizid(), $user->id));
421 $params = [
422 'quiz' => $quiz->get_quizid(),
423 'userid' => $user->id,
424 ];
425 $DB->delete_records('quiz_attempts', $params);
426 $DB->delete_records('quiz_grades', $params);
427}
428
25302dee
TH
429/**
430 * Get the best current grade for a particular user in a quiz.
431 *
432 * @param object $quiz the quiz settings.
f7970e3c 433 * @param int $userid the id of the user.
25a03faa 434 * @return float the user's current grade for this quiz, or null if this user does
25302dee
TH
435 * not have a grade on this quiz.
436 */
437function quiz_get_best_grade($quiz, $userid) {
438 global $DB;
25a03faa
TH
439 $grade = $DB->get_field('quiz_grades', 'grade',
440 array('quiz' => $quiz->id, 'userid' => $userid));
25302dee 441
b2607ccc 442 // Need to detect errors/no result, without catching 0 grades.
25302dee
TH
443 if ($grade === false) {
444 return null;
445 }
446
447 return $grade + 0; // Convert to number.
448}
449
450/**
451 * Is this a graded quiz? If this method returns true, you can assume that
452 * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to
453 * divide by them).
454 *
455 * @param object $quiz a row from the quiz table.
f7970e3c 456 * @return bool whether this is a graded quiz.
25302dee
TH
457 */
458function quiz_has_grades($quiz) {
459 return $quiz->grade >= 0.000005 && $quiz->sumgrades >= 0.000005;
460}
461
99caa248
JP
462/**
463 * Does this quiz allow multiple tries?
464 *
465 * @return bool
466 */
467function quiz_allows_multiple_tries($quiz) {
468 $bt = question_engine::get_behaviour_type($quiz->preferredbehaviour);
469 return $bt->allows_multiple_submitted_responses();
470}
471
8cc86111 472/**
473 * Return a small object with summary information about what a
474 * user has done with a given particular instance of this module
475 * Used for user activity reports.
476 * $return->time = the time they did it
477 * $return->info = a short text description
478 *
8cc86111 479 * @param object $course
480 * @param object $user
481 * @param object $mod
482 * @param object $quiz
483 * @return object|null
484 */
730fd187 485function quiz_user_outline($course, $user, $mod, $quiz) {
1a96363a 486 global $DB, $CFG;
172238f4 487 require_once($CFG->libdir . '/gradelib.php');
1a96363a
NC
488 $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
489
490 if (empty($grades->items[0]->grades)) {
491 return null;
492 } else {
493 $grade = reset($grades->items[0]->grades);
5ecfab51 494 }
495
0ff4bd08 496 $result = new stdClass();
930570b4
MC
497 // If the user can't see hidden grades, don't return that information.
498 $gitem = grade_item::fetch(array('id' => $grades->items[0]->id));
499 if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
500 $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
501 } else {
502 $result->info = get_string('grade') . ': ' . get_string('hidden', 'grades');
503 }
4433f871 504
9e83f3d1
TH
505 // Datesubmitted == time created. dategraded == time modified or time overridden
506 // if grade was last modified by the user themselves use date graded. Otherwise use
507 // date submitted.
508 // TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704.
4433f871
AD
509 if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
510 $result->time = $grade->dategraded;
511 } else {
512 $result->time = $grade->datesubmitted;
513 }
514
5ecfab51 515 return $result;
1a96363a 516}
730fd187 517
8cc86111 518/**
519 * Print a detailed representation of what a user has done with
520 * a given particular instance of this module, for user activity reports.
521 *
8cc86111 522 * @param object $course
523 * @param object $user
524 * @param object $mod
525 * @param object $quiz
526 * @return bool
527 */
730fd187 528function quiz_user_complete($course, $user, $mod, $quiz) {
1a14a14b 529 global $DB, $CFG, $OUTPUT;
be18f589 530 require_once($CFG->libdir . '/gradelib.php');
172238f4 531 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
25302dee 532
1a96363a
NC
533 $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
534 if (!empty($grades->items[0]->grades)) {
535 $grade = reset($grades->items[0]->grades);
930570b4
MC
536 // If the user can't see hidden grades, don't return that information.
537 $gitem = grade_item::fetch(array('id' => $grades->items[0]->id));
538 if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
539 echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
540 if ($grade->str_feedback) {
541 echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
542 }
543 } else {
544 echo $OUTPUT->container(get_string('grade') . ': ' . get_string('hidden', 'grades'));
545 if ($grade->str_feedback) {
546 echo $OUTPUT->container(get_string('feedback').': '.get_string('hidden', 'grades'));
547 }
1a96363a
NC
548 }
549 }
730fd187 550
25a03faa
TH
551 if ($attempts = $DB->get_records('quiz_attempts',
552 array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) {
ee1fb969 553 foreach ($attempts as $attempt) {
9378c99b 554 echo get_string('attempt', 'quiz', $attempt->attempt) . ': ';
be18f589
TH
555 if ($attempt->state != quiz_attempt::FINISHED) {
556 echo quiz_attempt_state_name($attempt->state);
ee1fb969 557 } else {
930570b4
MC
558 if (!isset($gitem)) {
559 if (!empty($grades->items[0]->grades)) {
560 $gitem = grade_item::fetch(array('id' => $grades->items[0]->id));
561 } else {
562 $gitem = new stdClass();
563 $gitem->hidden = true;
564 }
565 }
566 if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
567 echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' . quiz_format_grade($quiz, $quiz->sumgrades);
568 } else {
569 echo get_string('hidden', 'grades');
570 }
ee1fb969 571 }
572 echo ' - '.userdate($attempt->timemodified).'<br />';
573 }
574 } else {
25a03faa 575 print_string('noattempts', 'quiz');
ee1fb969 576 }
577
730fd187 578 return true;
579}
580
8cc86111 581/**
c2f5e2ab 582 * Quiz periodic clean-up tasks.
8cc86111 583 */
be0ba083 584function quiz_cron() {
be18f589 585 global $CFG;
c2f5e2ab 586
8e771aed
MP
587 require_once($CFG->dirroot . '/mod/quiz/cronlib.php');
588 mtrace('');
e1bf1619
TH
589
590 $timenow = time();
8e771aed 591 $overduehander = new mod_quiz_overdue_attempt_updater();
e1bf1619 592
8e771aed 593 $processto = $timenow - get_config('quiz', 'graceperiodmin');
e1bf1619 594
8e771aed 595 mtrace(' Looking for quiz overdue quiz attempts...');
815fcecc 596
8e771aed 597 list($count, $quizcount) = $overduehander->update_overdue_attempts($timenow, $processto);
815fcecc 598
8e771aed 599 mtrace(' Considered ' . $count . ' attempts in ' . $quizcount . ' quizzes.');
e1bf1619 600
c2f5e2ab
TH
601 // Run cron for our sub-plugin types.
602 cron_execute_plugin_type('quiz', 'quiz reports');
603 cron_execute_plugin_type('quizaccess', 'quiz access rules');
7b0f4a37
DM
604
605 return true;
730fd187 606}
607
b5a16eb7 608/**
ac032e0e 609 * @param int|array $quizids A quiz ID, or an array of quiz IDs.
f7970e3c 610 * @param int $userid the userid.
b5a16eb7 611 * @param string $status 'all', 'finished' or 'unfinished' to control
8cc86111 612 * @param bool $includepreviews
25a03faa
TH
613 * @return an array of all the user's attempts at this quiz. Returns an empty
614 * array if there are none.
b5a16eb7 615 */
ac032e0e 616function quiz_get_user_attempts($quizids, $userid, $status = 'finished', $includepreviews = false) {
36e48cb8
TH
617 global $DB, $CFG;
618 // TODO MDL-33071 it is very annoying to have to included all of locallib.php
619 // just to get the quiz_attempt::FINISHED constants, but I will try to sort
620 // that out properly for Moodle 2.4. For now, I will just do a quick fix for
621 // MDL-33048.
622 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
34b7d838
TH
623
624 $params = array();
625 switch ($status) {
626 case 'all':
627 $statuscondition = '';
628 break;
629
630 case 'finished':
631 $statuscondition = ' AND state IN (:state1, :state2)';
632 $params['state1'] = quiz_attempt::FINISHED;
633 $params['state2'] = quiz_attempt::ABANDONED;
634 break;
635
636 case 'unfinished':
637 $statuscondition = ' AND state IN (:state1, :state2)';
638 $params['state1'] = quiz_attempt::IN_PROGRESS;
639 $params['state2'] = quiz_attempt::OVERDUE;
640 break;
641 }
642
ac032e0e
FM
643 $quizids = (array) $quizids;
644 list($insql, $inparams) = $DB->get_in_or_equal($quizids, SQL_PARAMS_NAMED);
645 $params += $inparams;
646 $params['userid'] = $userid;
647
b5a16eb7 648 $previewclause = '';
649 if (!$includepreviews) {
650 $previewclause = ' AND preview = 0';
651 }
34b7d838 652
25302dee 653 return $DB->get_records_select('quiz_attempts',
ac032e0e 654 "quiz $insql AND userid = :userid" . $previewclause . $statuscondition,
8675453e 655 $params, 'quiz, attempt ASC');
b5a16eb7 656}
858deff0 657
d6dd2108 658/**
659 * Return grade for given user or all users.
660 *
661 * @param int $quizid id of quiz
662 * @param int $userid optional user id, 0 means all users
f88fb62c 663 * @return array array of grades, false if none. These are raw grades. They should
664 * be processed with quiz_format_grade for display.
d6dd2108 665 */
25302dee 666function quiz_get_user_grades($quiz, $userid = 0) {
9cf4a18b 667 global $CFG, $DB;
d6dd2108 668
9cf4a18b 669 $params = array($quiz->id);
25302dee 670 $usertest = '';
9cf4a18b 671 if ($userid) {
672 $params[] = $userid;
25302dee
TH
673 $usertest = 'AND u.id = ?';
674 }
8f37f7fb 675 return $DB->get_records_sql("
25302dee
TH
676 SELECT
677 u.id,
678 u.id AS userid,
679 qg.grade AS rawgrade,
680 qg.timemodified AS dategraded,
681 MAX(qa.timefinish) AS datesubmitted
682
683 FROM {user} u
684 JOIN {quiz_grades} qg ON u.id = qg.userid
685 JOIN {quiz_attempts} qa ON qa.quiz = qg.quiz AND qa.userid = u.id
686
8f37f7fb
TH
687 WHERE qg.quiz = ?
688 $usertest
689 GROUP BY u.id, qg.grade, qg.timemodified", $params);
d6dd2108 690}
691
f88fb62c 692/**
693 * Round a grade to to the correct number of decimal places, and format it for display.
694 *
695 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
696 * @param float $grade The grade to round.
8cc86111 697 * @return float
f88fb62c 698 */
699function quiz_format_grade($quiz, $grade) {
25302dee
TH
700 if (is_null($grade)) {
701 return get_string('notyetgraded', 'quiz');
702 }
f88fb62c 703 return format_float($grade, $quiz->decimalpoints);
704}
705
84e628a0 706/**
e1a2d0d9 707 * Determine the correct number of decimal places required to format a grade.
84e628a0 708 *
709 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
e1a2d0d9 710 * @return integer
84e628a0 711 */
e1a2d0d9 712function quiz_get_grade_format($quiz) {
25302dee
TH
713 if (empty($quiz->questiondecimalpoints)) {
714 $quiz->questiondecimalpoints = -1;
715 }
e1a2d0d9 716
84e628a0 717 if ($quiz->questiondecimalpoints == -1) {
e1a2d0d9 718 return $quiz->decimalpoints;
84e628a0 719 }
e1a2d0d9
CC
720
721 return $quiz->questiondecimalpoints;
722}
723
724/**
725 * Round a grade to the correct number of decimal places, and format it for display.
726 *
727 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
728 * @param float $grade The grade to round.
729 * @return float
730 */
731function quiz_format_question_grade($quiz, $grade) {
732 return format_float($grade, quiz_get_grade_format($quiz));
84e628a0 733}
734
d6dd2108 735/**
736 * Update grades in central gradebook
737 *
a153c9f2 738 * @category grade
25302dee
TH
739 * @param object $quiz the quiz settings.
740 * @param int $userid specific user only, 0 means all users.
a153c9f2 741 * @param bool $nullifnone If a single user is specified and $nullifnone is true a grade item with a null rawgrade will be inserted
d6dd2108 742 */
25302dee 743function quiz_update_grades($quiz, $userid = 0, $nullifnone = true) {
9cf4a18b 744 global $CFG, $DB;
172238f4 745 require_once($CFG->libdir . '/gradelib.php');
ed1daaa9 746
775f811a 747 if ($quiz->grade == 0) {
748 quiz_grade_item_update($quiz);
d6dd2108 749
775f811a 750 } else if ($grades = quiz_get_user_grades($quiz, $userid)) {
751 quiz_grade_item_update($quiz, $grades);
eafb9d9e 752
25302dee 753 } else if ($userid && $nullifnone) {
39790bd8 754 $grade = new stdClass();
25302dee
TH
755 $grade->userid = $userid;
756 $grade->rawgrade = null;
775f811a 757 quiz_grade_item_update($quiz, $grade);
d6dd2108 758
759 } else {
775f811a 760 quiz_grade_item_update($quiz);
761 }
762}
3b1d5cc4 763
d6dd2108 764/**
a153c9f2 765 * Create or update the grade item for given quiz
d6dd2108 766 *
a153c9f2 767 * @category grade
d6dd2108 768 * @param object $quiz object with extra cmidnumber
8cc86111 769 * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
d6dd2108 770 * @return int 0 if ok, error code otherwise
771 */
25a03faa 772function quiz_grade_item_update($quiz, $grades = null) {
3b1d5cc4 773 global $CFG, $OUTPUT;
609c0d6b 774 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
172238f4 775 require_once($CFG->libdir . '/gradelib.php');
d6dd2108 776
9e83f3d1 777 if (array_key_exists('cmidnumber', $quiz)) { // May not be always present.
25302dee 778 $params = array('itemname' => $quiz->name, 'idnumber' => $quiz->cmidnumber);
d6dd2108 779 } else {
25302dee 780 $params = array('itemname' => $quiz->name);
d6dd2108 781 }
782
783 if ($quiz->grade > 0) {
784 $params['gradetype'] = GRADE_TYPE_VALUE;
785 $params['grademax'] = $quiz->grade;
786 $params['grademin'] = 0;
787
788 } else {
789 $params['gradetype'] = GRADE_TYPE_NONE;
790 }
791
9e83f3d1 792 // What this is trying to do:
25a03faa
TH
793 // 1. If the quiz is set to not show grades while the quiz is still open,
794 // and is set to show grades after the quiz is closed, then create the
795 // grade_item with a show-after date that is the quiz close date.
796 // 2. If the quiz is set to not show grades at either of those times,
797 // create the grade_item as hidden.
798 // 3. If the quiz is set to show grades, create the grade_item visible.
25302dee
TH
799 $openreviewoptions = mod_quiz_display_options::make_from_quiz($quiz,
800 mod_quiz_display_options::LATER_WHILE_OPEN);
801 $closedreviewoptions = mod_quiz_display_options::make_from_quiz($quiz,
802 mod_quiz_display_options::AFTER_CLOSE);
6d03fd98
TH
803 if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX &&
804 $closedreviewoptions->marks < question_display_options::MARK_AND_MAX) {
1223d24a 805 $params['hidden'] = 1;
806
6d03fd98
TH
807 } else if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX &&
808 $closedreviewoptions->marks >= question_display_options::MARK_AND_MAX) {
1223d24a 809 if ($quiz->timeclose) {
810 $params['hidden'] = $quiz->timeclose;
811 } else {
812 $params['hidden'] = 1;
813 }
814
815 } else {
9e83f3d1 816 // Either
1223d24a 817 // a) both open and closed enabled
25a03faa 818 // b) open enabled, closed disabled - we can not "hide after",
9e83f3d1 819 // grades are kept visible even after closing.
1223d24a 820 $params['hidden'] = 0;
821 }
822
39873128
TH
823 if (!$params['hidden']) {
824 // If the grade item is not hidden by the quiz logic, then we need to
825 // hide it if the quiz is hidden from students.
1c73df9e
TH
826 if (property_exists($quiz, 'visible')) {
827 // Saving the quiz form, and cm not yet updated in the database.
828 $params['hidden'] = !$quiz->visible;
829 } else {
830 $cm = get_coursemodule_from_instance('quiz', $quiz->id);
831 $params['hidden'] = !$cm->visible;
832 }
39873128
TH
833 }
834
0b5a80a1 835 if ($grades === 'reset') {
836 $params['reset'] = true;
25a03faa 837 $grades = null;
0b5a80a1 838 }
9cf4a18b 839
49460d84 840 $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
d45459b7 841 if (!empty($gradebook_grades->items)) {
842 $grade_item = $gradebook_grades->items[0];
843 if ($grade_item->locked) {
9e83f3d1 844 // NOTE: this is an extremely nasty hack! It is not a bug if this confirmation fails badly. --skodak.
d45459b7 845 $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
846 if (!$confirm_regrade) {
f5f36584
MP
847 if (!AJAX_SCRIPT) {
848 $message = get_string('gradeitemislocked', 'grades');
849 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id .
850 '&amp;mode=overview';
851 $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
852 echo $OUTPUT->box_start('generalbox', 'notice');
853 echo '<p>'. $message .'</p>';
854 echo $OUTPUT->container_start('buttons');
855 echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades'));
856 echo $OUTPUT->single_button($back_link, get_string('cancel'));
857 echo $OUTPUT->container_end();
858 echo $OUTPUT->box_end();
859 }
d45459b7 860 return GRADE_UPDATE_ITEM_LOCKED;
861 }
49460d84 862 }
863 }
0b5a80a1 864
ced5ee59 865 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
d6dd2108 866}
867
868/**
869 * Delete grade item for given quiz
870 *
a153c9f2 871 * @category grade
d6dd2108 872 * @param object $quiz object
873 * @return object quiz
874 */
875function quiz_grade_item_delete($quiz) {
876 global $CFG;
53004e48 877 require_once($CFG->libdir . '/gradelib.php');
d6dd2108 878
25a03faa
TH
879 return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0,
880 null, array('deleted' => 1));
d6dd2108 881}
882
8cc86111 883/**
884 * This standard function will check all instances of this module
885 * and make sure there are up-to-date events created for each of them.
886 * If courseid = 0, then every quiz event in the site is checked, else
887 * only quiz events belonging to the course specified are checked.
888 * This function is used, in its new format, by restore_refresh_events()
889 *
8cc86111 890 * @param int $courseid
9ffbdafc
AG
891 * @param int|stdClass $instance Quiz module instance or ID.
892 * @param int|stdClass $cm Course module object or ID (not used in this module).
8cc86111 893 * @return bool
894 */
9ffbdafc 895function quiz_refresh_events($courseid = 0, $instance = null, $cm = null) {
9cf4a18b 896 global $DB;
d2f308c0 897
9ffbdafc
AG
898 // If we have instance information then we can just update the one event instead of updating all events.
899 if (isset($instance)) {
900 if (!is_object($instance)) {
901 $instance = $DB->get_record('quiz', array('id' => $instance), '*', MUST_EXIST);
902 }
903 quiz_update_events($instance);
904 return true;
905 }
906
d2f308c0 907 if ($courseid == 0) {
25302dee 908 if (!$quizzes = $DB->get_records('quiz')) {
d2f308c0 909 return true;
910 }
911 } else {
25302dee 912 if (!$quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
d2f308c0 913 return true;
914 }
915 }
f41e824f 916
d2f308c0 917 foreach ($quizzes as $quiz) {
990650f9 918 quiz_update_events($quiz);
d2f308c0 919 }
990650f9 920
d2f308c0 921 return true;
922}
923
dd97c328 924/**
925 * Returns all quiz graded users since a given time for specified quiz
926 */
8d297188
TH
927function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
928 $courseid, $cmid, $userid = 0, $groupid = 0) {
e19c086c 929 global $CFG, $USER, $DB;
172238f4 930 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
6710ec87 931
e19c086c 932 $course = get_course($courseid);
f20edd52 933 $modinfo = get_fast_modinfo($course);
6710ec87 934
dd97c328 935 $cm = $modinfo->cms[$cmid];
8d297188 936 $quiz = $DB->get_record('quiz', array('id' => $cm->instance));
9cf4a18b 937
dd97c328 938 if ($userid) {
8d297188
TH
939 $userselect = "AND u.id = :userid";
940 $params['userid'] = $userid;
dd97c328 941 } else {
8d297188 942 $userselect = '';
dd97c328 943 }
ac21ad39 944
dd97c328 945 if ($groupid) {
8d297188
TH
946 $groupselect = 'AND gm.groupid = :groupid';
947 $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id';
948 $params['groupid'] = $groupid;
dd97c328 949 } else {
8d297188
TH
950 $groupselect = '';
951 $groupjoin = '';
952 }
953
122fc5d9
DM
954 $params['timestart'] = $timestart;
955 $params['quizid'] = $quiz->id;
956
21d22803 957 $ufields = user_picture::fields('u', null, 'useridagain');
8d297188
TH
958 if (!$attempts = $DB->get_records_sql("
959 SELECT qa.*,
3047f140 960 {$ufields}
8d297188
TH
961 FROM {quiz_attempts} qa
962 JOIN {user} u ON u.id = qa.userid
963 $groupjoin
964 WHERE qa.timefinish > :timestart
965 AND qa.quiz = :quizid
966 AND qa.preview = 0
967 $userselect
968 $groupselect
969 ORDER BY qa.timefinish ASC", $params)) {
970 return;
971 }
972
c492a78e 973 $context = context_module::instance($cm->id);
8d297188
TH
974 $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
975 $viewfullnames = has_capability('moodle/site:viewfullnames', $context);
cb323d02 976 $grader = has_capability('mod/quiz:viewreports', $context);
dd97c328 977 $groupmode = groups_get_activity_groupmode($cm, $course);
6710ec87 978
8d297188 979 $usersgroups = null;
25a03faa 980 $aname = format_string($cm->name, true);
dd97c328 981 foreach ($attempts as $attempt) {
982 if ($attempt->userid != $USER->id) {
983 if (!$grader) {
9e83f3d1 984 // Grade permission required.
dd97c328 985 continue;
986 }
6710ec87 987
9cf4a18b 988 if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
e19c086c
MG
989 $usersgroups = groups_get_all_groups($course->id,
990 $attempt->userid, $cm->groupingid);
991 $usersgroups = array_keys($usersgroups);
992 if (!array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid))) {
dd97c328 993 continue;
994 }
995 }
8d297188
TH
996 }
997
7ee80cab 998 $options = quiz_get_review_options($quiz, $attempt, $context);
dd97c328 999
0ff4bd08 1000 $tmpactivity = new stdClass();
dd97c328 1001
8d297188
TH
1002 $tmpactivity->type = 'quiz';
1003 $tmpactivity->cmid = $cm->id;
1004 $tmpactivity->name = $aname;
1005 $tmpactivity->sectionnum = $cm->sectionnum;
1006 $tmpactivity->timestamp = $attempt->timefinish;
9cf4a18b 1007
a957e97f 1008 $tmpactivity->content = new stdClass();
dd97c328 1009 $tmpactivity->content->attemptid = $attempt->id;
dd97c328 1010 $tmpactivity->content->attempt = $attempt->attempt;
6d03fd98 1011 if (quiz_has_grades($quiz) && $options->marks >= question_display_options::MARK_AND_MAX) {
8d297188
TH
1012 $tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades);
1013 $tmpactivity->content->maxgrade = quiz_format_grade($quiz, $quiz->sumgrades);
1014 } else {
1015 $tmpactivity->content->sumgrades = null;
1016 $tmpactivity->content->maxgrade = null;
1017 }
9cf4a18b 1018
21d22803 1019 $tmpactivity->user = user_picture::unalias($attempt, null, 'useridagain');
3047f140 1020 $tmpactivity->user->fullname = fullname($tmpactivity->user, $viewfullnames);
9cf4a18b 1021
dd97c328 1022 $activities[$index++] = $tmpactivity;
6710ec87 1023 }
6710ec87 1024}
1025
dd97c328 1026function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
e63f88c9 1027 global $CFG, $OUTPUT;
6710ec87 1028
dd97c328 1029 echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">';
6710ec87 1030
8d297188
TH
1031 echo '<tr><td class="userpicture" valign="top">';
1032 echo $OUTPUT->user_picture($activity->user, array('courseid' => $courseid));
1033 echo '</td><td>';
6710ec87 1034
1035 if ($detail) {
dd97c328 1036 $modname = $modnames[$activity->type];
1037 echo '<div class="title">';
3e6adcd6 1038 echo $OUTPUT->image_icon('icon', $modname, $activity->type);
8d297188
TH
1039 echo '<a href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
1040 $activity->cmid . '">' . $activity->name . '</a>';
dd97c328 1041 echo '</div>';
6710ec87 1042 }
1043
dd97c328 1044 echo '<div class="grade">';
8d297188
TH
1045 echo get_string('attempt', 'quiz', $activity->content->attempt);
1046 if (isset($activity->content->maxgrade)) {
1047 $grades = $activity->content->sumgrades . ' / ' . $activity->content->maxgrade;
1048 echo ': (<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?attempt=' .
1049 $activity->content->attemptid . '">' . $grades . '</a>)';
1050 }
dd97c328 1051 echo '</div>';
6710ec87 1052
dd97c328 1053 echo '<div class="user">';
8d297188
TH
1054 echo '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $activity->user->id .
1055 '&amp;course=' . $courseid . '">' . $activity->user->fullname .
1056 '</a> - ' . userdate($activity->timestamp);
dd97c328 1057 echo '</div>';
6710ec87 1058
8d297188 1059 echo '</td></tr></table>';
6710ec87 1060
1061 return;
1062}
1063
ee1fb969 1064/**
920b93d1 1065 * Pre-process the quiz options form data, making any necessary adjustments.
ad4cd837 1066 * Called by add/update instance in this file.
b159da78 1067 *
920b93d1 1068 * @param object $quiz The variables set on the form.
1069 */
25302dee
TH
1070function quiz_process_options($quiz) {
1071 global $CFG;
1072 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
1073 require_once($CFG->libdir . '/questionlib.php');
1074
920b93d1 1075 $quiz->timemodified = time();
ee1fb969 1076
dc5c6851 1077 // Quiz name.
1078 if (!empty($quiz->name)) {
1079 $quiz->name = trim($quiz->name);
1080 }
a23f0aaf 1081
ab0a8ff2 1082 // Password field - different in form to stop browsers that remember passwords
1083 // getting confused.
1084 $quiz->password = $quiz->quizpassword;
1085 unset($quiz->quizpassword);
1086
9e83f3d1 1087 // Quiz feedback.
a0807a00 1088 if (isset($quiz->feedbacktext)) {
1089 // Clean up the boundary text.
1090 for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
fe6ce234
DC
1091 if (empty($quiz->feedbacktext[$i]['text'])) {
1092 $quiz->feedbacktext[$i]['text'] = '';
a0807a00 1093 } else {
fe6ce234 1094 $quiz->feedbacktext[$i]['text'] = trim($quiz->feedbacktext[$i]['text']);
a0807a00 1095 }
212b7b8c 1096 }
b159da78 1097
a0807a00 1098 // Check the boundary value is a number or a percentage, and in range.
1099 $i = 0;
1100 while (!empty($quiz->feedbackboundaries[$i])) {
1101 $boundary = trim($quiz->feedbackboundaries[$i]);
1102 if (!is_numeric($boundary)) {
1103 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
1104 $boundary = trim(substr($boundary, 0, -1));
1105 if (is_numeric($boundary)) {
1106 $boundary = $boundary * $quiz->grade / 100.0;
1107 } else {
1108 return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
1109 }
212b7b8c 1110 }
1111 }
a0807a00 1112 if ($boundary <= 0 || $boundary >= $quiz->grade) {
1113 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
1114 }
1115 if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
1116 return get_string('feedbackerrororder', 'quiz', $i + 1);
1117 }
1118 $quiz->feedbackboundaries[$i] = $boundary;
1119 $i += 1;
212b7b8c 1120 }
a0807a00 1121 $numboundaries = $i;
b159da78 1122
a0807a00 1123 // Check there is nothing in the remaining unused fields.
e0b7cfcb 1124 if (!empty($quiz->feedbackboundaries)) {
1125 for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
25a03faa
TH
1126 if (!empty($quiz->feedbackboundaries[$i]) &&
1127 trim($quiz->feedbackboundaries[$i]) != '') {
e0b7cfcb 1128 return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
1129 }
a0807a00 1130 }
212b7b8c 1131 }
a0807a00 1132 for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
25a03faa
TH
1133 if (!empty($quiz->feedbacktext[$i]['text']) &&
1134 trim($quiz->feedbacktext[$i]['text']) != '') {
a0807a00 1135 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
1136 }
212b7b8c 1137 }
25a03faa
TH
1138 // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
1139 $quiz->feedbackboundaries[-1] = $quiz->grade + 1;
a0807a00 1140 $quiz->feedbackboundaries[$numboundaries] = 0;
1141 $quiz->feedbackboundarycount = $numboundaries;
2cf5ab10
TH
1142 } else {
1143 $quiz->feedbackboundarycount = -1;
212b7b8c 1144 }
a23f0aaf 1145
25302dee
TH
1146 // Combing the individual settings into the review columns.
1147 $quiz->reviewattempt = quiz_review_option_form_to_db($quiz, 'attempt');
1148 $quiz->reviewcorrectness = quiz_review_option_form_to_db($quiz, 'correctness');
1149 $quiz->reviewmarks = quiz_review_option_form_to_db($quiz, 'marks');
1150 $quiz->reviewspecificfeedback = quiz_review_option_form_to_db($quiz, 'specificfeedback');
1151 $quiz->reviewgeneralfeedback = quiz_review_option_form_to_db($quiz, 'generalfeedback');
1152 $quiz->reviewrightanswer = quiz_review_option_form_to_db($quiz, 'rightanswer');
1153 $quiz->reviewoverallfeedback = quiz_review_option_form_to_db($quiz, 'overallfeedback');
1154 $quiz->reviewattempt |= mod_quiz_display_options::DURING;
1155 $quiz->reviewoverallfeedback &= ~mod_quiz_display_options::DURING;
1156}
ee1fb969 1157
25302dee
TH
1158/**
1159 * Helper function for {@link quiz_process_options()}.
1160 * @param object $fromform the sumbitted form date.
1161 * @param string $field one of the review option field names.
1162 */
1163function quiz_review_option_form_to_db($fromform, $field) {
1164 static $times = array(
1165 'during' => mod_quiz_display_options::DURING,
1166 'immediately' => mod_quiz_display_options::IMMEDIATELY_AFTER,
1167 'open' => mod_quiz_display_options::LATER_WHILE_OPEN,
1168 'closed' => mod_quiz_display_options::AFTER_CLOSE,
1169 );
00719c02 1170
25302dee
TH
1171 $review = 0;
1172 foreach ($times as $whenname => $when) {
1173 $fieldname = $field . $whenname;
1174 if (isset($fromform->$fieldname)) {
1175 $review |= $when;
1176 unset($fromform->$fieldname);
1177 }
1b8a7434 1178 }
1179
25302dee 1180 return $review;
920b93d1 1181}
1182
1183/**
1184 * This function is called at the end of quiz_add_instance
1185 * and quiz_update_instance, to do the common processing.
a23f0aaf 1186 *
920b93d1 1187 * @param object $quiz the quiz object.
1188 */
1189function quiz_after_add_or_update($quiz) {
c18269c7 1190 global $DB;
fe6ce234
DC
1191 $cmid = $quiz->coursemodule;
1192
9e83f3d1 1193 // We need to use context now, so we need to make sure all needed info is already in db.
fe6ce234 1194 $DB->set_field('course_modules', 'instance', $quiz->id, array('id'=>$cmid));
c492a78e 1195 $context = context_module::instance($cmid);
920b93d1 1196
9e83f3d1 1197 // Save the feedback.
53004e48 1198 $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
a23f0aaf 1199
fe6ce234 1200 for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) {
0ff4bd08 1201 $feedback = new stdClass();
212b7b8c 1202 $feedback->quizid = $quiz->id;
fe6ce234
DC
1203 $feedback->feedbacktext = $quiz->feedbacktext[$i]['text'];
1204 $feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format'];
212b7b8c 1205 $feedback->mingrade = $quiz->feedbackboundaries[$i];
1206 $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
fe6ce234 1207 $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
25a03faa
TH
1208 $feedbacktext = file_save_draft_area_files((int)$quiz->feedbacktext[$i]['itemid'],
1209 $context->id, 'mod_quiz', 'feedback', $feedback->id,
1210 array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0),
1211 $quiz->feedbacktext[$i]['text']);
1212 $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext,
1213 array('id' => $feedback->id));
212b7b8c 1214 }
1215
b83c32d3
TH
1216 // Store any settings belonging to the access rules.
1217 quiz_access_manager::save_settings($quiz);
1218
920b93d1 1219 // Update the events relating to this quiz.
990650f9 1220 quiz_update_events($quiz);
9ffbdafc
AG
1221 $completionexpected = (!empty($quiz->completionexpected)) ? $quiz->completionexpected : null;
1222 \core_completion\api::update_completion_date_event($quiz->coursemodule, 'quiz', $quiz->id, $completionexpected);
990650f9 1223
9e83f3d1 1224 // Update related grade item.
990650f9 1225 quiz_grade_item_update($quiz);
990650f9
TH
1226}
1227
1228/**
1229 * This function updates the events associated to the quiz.
1230 * If $override is non-zero, then it updates only the events
1231 * associated with the specified override.
1232 *
1233 * @uses QUIZ_MAX_EVENT_LENGTH
1234 * @param object $quiz the quiz object.
1235 * @param object optional $override limit to a specific override
1236 */
1237function quiz_update_events($quiz, $override = null) {
1238 global $DB;
1239
1240 // Load the old events relating to this quiz.
1241 $conds = array('modulename'=>'quiz',
1242 'instance'=>$quiz->id);
1243 if (!empty($override)) {
9e83f3d1 1244 // Only load events for this override.
cc4fba86
MN
1245 if (isset($override->userid)) {
1246 $conds['userid'] = $override->userid;
1247 } else {
1248 $conds['groupid'] = $override->groupid;
1249 }
990650f9 1250 }
4133df6f 1251 $oldevents = $DB->get_records('event', $conds, 'id ASC');
990650f9 1252
98239b2d 1253 // Now make a to-do list of all that needs to be updated.
990650f9 1254 if (empty($override)) {
4133df6f
CB
1255 // We are updating the primary settings for the quiz, so we need to add all the overrides.
1256 $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id ASC');
1257 // It is necessary to add an empty stdClass to the beginning of the array as the $oldevents
1258 // list contains the original (non-override) event for the module. If this is not included
1259 // the logic below will end up updating the wrong row when we try to reconcile this $overrides
1260 // list against the $oldevents list.
1261 array_unshift($overrides, new stdClass());
25a03faa 1262 } else {
9e83f3d1 1263 // Just do the one override.
990650f9 1264 $overrides = array($override);
920b93d1 1265 }
1266
98239b2d
JP
1267 // Get group override priorities.
1268 $grouppriorities = quiz_get_group_override_priorities($quiz->id);
1269
990650f9
TH
1270 foreach ($overrides as $current) {
1271 $groupid = isset($current->groupid)? $current->groupid : 0;
1272 $userid = isset($current->userid)? $current->userid : 0;
1273 $timeopen = isset($current->timeopen)? $current->timeopen : $quiz->timeopen;
1274 $timeclose = isset($current->timeclose)? $current->timeclose : $quiz->timeclose;
1275
9e83f3d1 1276 // Only add open/close events for an override if they differ from the quiz default.
990650f9
TH
1277 $addopen = empty($current->id) || !empty($current->timeopen);
1278 $addclose = empty($current->id) || !empty($current->timeclose);
1279
2079c732
TH
1280 if (!empty($quiz->coursemodule)) {
1281 $cmid = $quiz->coursemodule;
1282 } else {
393cadf2 1283 $cmid = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course)->id;
2079c732
TH
1284 }
1285
0ff4bd08 1286 $event = new stdClass();
b3d7d175 1287 $event->type = !$timeclose ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD;
2079c732 1288 $event->description = format_module_intro('quiz', $quiz, $cmid);
9e83f3d1 1289 // Events module won't show user events when the courseid is nonzero.
25a03faa 1290 $event->courseid = ($userid) ? 0 : $quiz->course;
990650f9
TH
1291 $event->groupid = $groupid;
1292 $event->userid = $userid;
1293 $event->modulename = 'quiz';
1294 $event->instance = $quiz->id;
1295 $event->timestart = $timeopen;
1296 $event->timeduration = max($timeclose - $timeopen, 0);
b3d7d175 1297 $event->timesort = $timeopen;
990650f9 1298 $event->visible = instance_is_visible('quiz', $quiz);
b3d7d175 1299 $event->eventtype = QUIZ_EVENT_TYPE_OPEN;
4133df6f 1300 $event->priority = null;
990650f9 1301
98239b2d 1302 // Determine the event name and priority.
990650f9 1303 if ($groupid) {
98239b2d 1304 // Group override event.
0ff4bd08 1305 $params = new stdClass();
990650f9
TH
1306 $params->quiz = $quiz->name;
1307 $params->group = groups_get_group_name($groupid);
1308 if ($params->group === false) {
9e83f3d1 1309 // Group doesn't exist, just skip it.
990650f9
TH
1310 continue;
1311 }
1312 $eventname = get_string('overridegroupeventname', 'quiz', $params);
98239b2d
JP
1313 // Set group override priority.
1314 if ($grouppriorities !== null) {
1315 $openpriorities = $grouppriorities['open'];
1316 if (isset($openpriorities[$timeopen])) {
1317 $event->priority = $openpriorities[$timeopen];
1318 }
1319 }
25a03faa 1320 } else if ($userid) {
98239b2d 1321 // User override event.
0ff4bd08 1322 $params = new stdClass();
990650f9
TH
1323 $params->quiz = $quiz->name;
1324 $eventname = get_string('overrideusereventname', 'quiz', $params);
98239b2d
JP
1325 // Set user override priority.
1326 $event->priority = CALENDAR_EVENT_USER_OVERRIDE_PRIORITY;
990650f9 1327 } else {
98239b2d 1328 // The parent event.
990650f9
TH
1329 $eventname = $quiz->name;
1330 }
98239b2d 1331
990650f9 1332 if ($addopen or $addclose) {
98239b2d
JP
1333 // Separate start and end events.
1334 $event->timeduration = 0;
1335 if ($timeopen && $addopen) {
990650f9
TH
1336 if ($oldevent = array_shift($oldevents)) {
1337 $event->id = $oldevent->id;
25a03faa 1338 } else {
990650f9
TH
1339 unset($event->id);
1340 }
f10721d6 1341 $event->name = get_string('quizeventopens', 'quiz', $eventname);
9e83f3d1 1342 // The method calendar_event::create will reuse a db record if the id field is set.
990650f9 1343 calendar_event::create($event);
98239b2d
JP
1344 }
1345 if ($timeclose && $addclose) {
1346 if ($oldevent = array_shift($oldevents)) {
1347 $event->id = $oldevent->id;
1348 } else {
1349 unset($event->id);
990650f9 1350 }
b3d7d175 1351 $event->type = CALENDAR_EVENT_TYPE_ACTION;
f10721d6 1352 $event->name = get_string('quizeventcloses', 'quiz', $eventname);
98239b2d 1353 $event->timestart = $timeclose;
b3d7d175
MN
1354 $event->timesort = $timeclose;
1355 $event->eventtype = QUIZ_EVENT_TYPE_CLOSE;
98239b2d
JP
1356 if ($groupid && $grouppriorities !== null) {
1357 $closepriorities = $grouppriorities['close'];
1358 if (isset($closepriorities[$timeclose])) {
1359 $event->priority = $closepriorities[$timeclose];
990650f9 1360 }
990650f9 1361 }
98239b2d 1362 calendar_event::create($event);
990650f9 1363 }
920b93d1 1364 }
1365 }
d6dd2108 1366
9e83f3d1 1367 // Delete any leftover events.
990650f9
TH
1368 foreach ($oldevents as $badevent) {
1369 $badevent = calendar_event::load($badevent);
1370 $badevent->delete();
1371 }
ee1fb969 1372}
1373
efe24976
JP
1374/**
1375 * Calculates the priorities of timeopen and timeclose values for group overrides for a quiz.
1376 *
1377 * @param int $quizid The quiz ID.
1378 * @return array|null Array of group override priorities for open and close times. Null if there are no group overrides.
1379 */
1380function quiz_get_group_override_priorities($quizid) {
1381 global $DB;
1382
1383 // Fetch group overrides.
1384 $where = 'quiz = :quiz AND groupid IS NOT NULL';
1385 $params = ['quiz' => $quizid];
1386 $overrides = $DB->get_records_select('quiz_overrides', $where, $params, '', 'id, timeopen, timeclose');
1387 if (!$overrides) {
1388 return null;
1389 }
1390
1391 $grouptimeopen = [];
1392 $grouptimeclose = [];
1393 foreach ($overrides as $override) {
1394 if ($override->timeopen !== null && !in_array($override->timeopen, $grouptimeopen)) {
1395 $grouptimeopen[] = $override->timeopen;
1396 }
1397 if ($override->timeclose !== null && !in_array($override->timeclose, $grouptimeclose)) {
1398 $grouptimeclose[] = $override->timeclose;
1399 }
1400 }
1401
7eae70e1
CB
1402 // Sort open times in ascending manner. The earlier open time gets higher priority.
1403 sort($grouptimeopen);
efe24976
JP
1404 // Set priorities.
1405 $opengrouppriorities = [];
1406 $openpriority = 1;
1407 foreach ($grouptimeopen as $timeopen) {
1408 $opengrouppriorities[$timeopen] = $openpriority++;
1409 }
1410
7eae70e1
CB
1411 // Sort close times in descending manner. The later close time gets higher priority.
1412 rsort($grouptimeclose);
efe24976
JP
1413 // Set priorities.
1414 $closegrouppriorities = [];
1415 $closepriority = 1;
1416 foreach ($grouptimeclose as $timeclose) {
1417 $closegrouppriorities[$timeclose] = $closepriority++;
1418 }
1419
1420 return [
1421 'open' => $opengrouppriorities,
1422 'close' => $closegrouppriorities
1423 ];
1424}
1425
8cc86111 1426/**
b2b4ec30
RT
1427 * List the actions that correspond to a view of this module.
1428 * This is used by the participation report.
1429 *
1430 * Note: This is not used by new logging system. Event with
1431 * crud = 'r' and edulevel = LEVEL_PARTICIPATING will
1432 * be considered as view action.
1433 *
8cc86111 1434 * @return array
1435 */
f3221af9 1436function quiz_get_view_actions() {
cf051cc4 1437 return array('view', 'view all', 'report', 'review');
f3221af9 1438}
ee1fb969 1439
8cc86111 1440/**
b2b4ec30
RT
1441 * List the actions that correspond to a post of this module.
1442 * This is used by the participation report.
1443 *
1444 * Note: This is not used by new logging system. Event with
1445 * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING
1446 * will be considered as post action.
1447 *
8cc86111 1448 * @return array
1449 */
f3221af9 1450function quiz_get_post_actions() {
25a03faa
TH
1451 return array('attempt', 'close attempt', 'preview', 'editquestions',
1452 'delete attempt', 'manualgrade');
f3221af9 1453}
ee1fb969 1454
f67172b6 1455/**
25302dee 1456 * @param array $questionids of question ids.
f7970e3c 1457 * @return bool whether any of these questions are used by any instance of this module.
f67172b6 1458 */
25302dee 1459function quiz_questions_in_use($questionids) {
07f88584
TH
1460 global $DB, $CFG;
1461 require_once($CFG->libdir . '/questionlib.php');
25302dee 1462 list($test, $params) = $DB->get_in_or_equal($questionids);
ccba5b88 1463 return $DB->record_exists_select('quiz_slots',
dc4a3ea1 1464 'questionid ' . $test, $params) || question_engine::questions_in_use(
6b5f24d3 1465 $questionids, new qubaid_join('{quiz_attempts} quiza',
07f88584 1466 'quiza.uniqueid', 'quiza.preview = 0'));
f67172b6 1467}
1468
7a6f4066 1469/**
1470 * Implementation of the function for printing the form elements that control
1471 * whether the course reset functionality affects the quiz.
3b1d5cc4 1472 *
25302dee 1473 * @param $mform the course reset form that is being built.
0b5a80a1 1474 */
25302dee 1475function quiz_reset_course_form_definition($mform) {
c159da4c 1476 $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz'));
25a03faa
TH
1477 $mform->addElement('advcheckbox', 'reset_quiz_attempts',
1478 get_string('removeallquizattempts', 'quiz'));
113486d2
SB
1479 $mform->addElement('advcheckbox', 'reset_quiz_user_overrides',
1480 get_string('removealluseroverrides', 'quiz'));
1481 $mform->addElement('advcheckbox', 'reset_quiz_group_overrides',
1482 get_string('removeallgroupoverrides', 'quiz'));
0b5a80a1 1483}
1484
1485/**
1486 * Course reset form defaults.
25302dee 1487 * @return array the defaults.
0b5a80a1 1488 */
1489function quiz_reset_course_form_defaults($course) {
113486d2
SB
1490 return array('reset_quiz_attempts' => 1,
1491 'reset_quiz_group_overrides' => 1,
1492 'reset_quiz_user_overrides' => 1);
0b5a80a1 1493}
1494
1495/**
1496 * Removes all grades from gradebook
8cc86111 1497 *
0b5a80a1 1498 * @param int $courseid
1499 * @param string optional type
7a6f4066 1500 */
0b5a80a1 1501function quiz_reset_gradebook($courseid, $type='') {
9cf4a18b 1502 global $CFG, $DB;
0b5a80a1 1503
25302dee
TH
1504 $quizzes = $DB->get_records_sql("
1505 SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
1506 FROM {modules} m
1507 JOIN {course_modules} cm ON m.id = cm.module
1508 JOIN {quiz} q ON cm.instance = q.id
1509 WHERE m.name = 'quiz' AND cm.course = ?", array($courseid));
0b5a80a1 1510
25302dee
TH
1511 foreach ($quizzes as $quiz) {
1512 quiz_grade_item_update($quiz, 'reset');
0b5a80a1 1513 }
7a6f4066 1514}
1515
1516/**
72d2982e 1517 * Actual implementation of the reset course functionality, delete all the
7a6f4066 1518 * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is
1519 * set and true.
6ef56c99 1520 *
1521 * Also, move the quiz open and close dates, if the course start date is changing.
8cc86111 1522 *
8cc86111 1523 * @param object $data the data submitted from the reset course.
0b5a80a1 1524 * @return array status array
7a6f4066 1525 */
0b5a80a1 1526function quiz_reset_userdata($data) {
53004e48 1527 global $CFG, $DB;
172238f4 1528 require_once($CFG->libdir . '/questionlib.php');
be0ba083 1529
0b5a80a1 1530 $componentstr = get_string('modulenameplural', 'quiz');
1531 $status = array();
b159da78 1532
25a03faa 1533 // Delete attempts.
6ef56c99 1534 if (!empty($data->reset_quiz_attempts)) {
6b5f24d3
TH
1535 question_engine::delete_questions_usage_by_activities(new qubaid_join(
1536 '{quiz_attempts} quiza JOIN {quiz} quiz ON quiza.quiz = quiz.id',
1537 'quiza.uniqueid', 'quiz.course = :quizcourseid',
1538 array('quizcourseid' => $data->courseid)));
25302dee
TH
1539
1540 $DB->delete_records_select('quiz_attempts',
1541 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid));
1542 $status[] = array(
1543 'component' => $componentstr,
25a03faa 1544 'item' => get_string('attemptsdeleted', 'quiz'),
25302dee
TH
1545 'error' => false);
1546
9e83f3d1 1547 // Remove all grades from gradebook.
8dd9ccf4
TH
1548 $DB->delete_records_select('quiz_grades',
1549 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid));
0b5a80a1 1550 if (empty($data->reset_gradebook_grades)) {
1551 quiz_reset_gradebook($data->courseid);
7a6f4066 1552 }
25302dee
TH
1553 $status[] = array(
1554 'component' => $componentstr,
8dd9ccf4 1555 'item' => get_string('gradesdeleted', 'quiz'),
25302dee 1556 'error' => false);
7a6f4066 1557 }
6ef56c99 1558
113486d2
SB
1559 // Remove user overrides.
1560 if (!empty($data->reset_quiz_user_overrides)) {
1561 $DB->delete_records_select('quiz_overrides',
1562 'quiz IN (SELECT id FROM {quiz} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid));
1563 $status[] = array(
1564 'component' => $componentstr,
1565 'item' => get_string('useroverridesdeleted', 'quiz'),
1566 'error' => false);
1567 }
1568 // Remove group overrides.
1569 if (!empty($data->reset_quiz_group_overrides)) {
1570 $DB->delete_records_select('quiz_overrides',
1571 'quiz IN (SELECT id FROM {quiz} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid));
1572 $status[] = array(
1573 'component' => $componentstr,
1574 'item' => get_string('groupoverridesdeleted', 'quiz'),
1575 'error' => false);
1576 }
1577
9e83f3d1 1578 // Updating dates - shift may be negative too.
0b5a80a1 1579 if ($data->timeshift) {
2079c732
TH
1580 $DB->execute("UPDATE {quiz_overrides}
1581 SET timeopen = timeopen + ?
1582 WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?)
1583 AND timeopen <> 0", array($data->timeshift, $data->courseid));
1584 $DB->execute("UPDATE {quiz_overrides}
1585 SET timeclose = timeclose + ?
1586 WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?)
1587 AND timeclose <> 0", array($data->timeshift, $data->courseid));
1588
0d14fcbc
AA
1589 // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
1590 // See MDL-9367.
25a03faa
TH
1591 shift_course_mod_dates('quiz', array('timeopen', 'timeclose'),
1592 $data->timeshift, $data->courseid);
2079c732 1593
25302dee
TH
1594 $status[] = array(
1595 'component' => $componentstr,
1596 'item' => get_string('openclosedatesupdated', 'quiz'),
1597 'error' => false);
7a6f4066 1598 }
0b5a80a1 1599
1600 return $status;
7a6f4066 1601}
14e6dc79 1602
b5a16eb7 1603/**
1604 * Prints quiz summaries on MyMoodle Page
e9dfeec9
MN
1605 *
1606 * @deprecated since 3.3
063b7ee6
MN
1607 * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
1608 * @param array $courses
8cc86111 1609 * @param array $htmlarray
b5a16eb7 1610 */
1611function quiz_print_overview($courses, &$htmlarray) {
1612 global $USER, $CFG;
e9dfeec9
MN
1613
1614 debugging('The function quiz_print_overview() is now deprecated.', DEBUG_DEVELOPER);
1615
9e83f3d1 1616 // These next 6 Lines are constant in all modules (just change module name).
b5a16eb7 1617 if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1618 return array();
1619 }
1620
2a13e454 1621 if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
b5a16eb7 1622 return;
1623 }
1624
ac032e0e
FM
1625 // Get the quizzes attempts.
1626 $attemptsinfo = [];
1627 $quizids = [];
1628 foreach ($quizzes as $quiz) {
1629 $quizids[] = $quiz->id;
1630 $attemptsinfo[$quiz->id] = ['count' => 0, 'hasfinished' => false];
1631 }
1632 $attempts = quiz_get_user_attempts($quizids, $USER->id);
1633 foreach ($attempts as $attempt) {
1634 $attemptsinfo[$attempt->quiz]['count']++;
1635 $attemptsinfo[$attempt->quiz]['hasfinished'] = true;
1636 }
1637 unset($attempts);
1638
25a03faa 1639 // Fetch some language strings outside the main loop.
b5a16eb7 1640 $strquiz = get_string('modulename', 'quiz');
1641 $strnoattempts = get_string('noattempts', 'quiz');
1642
25a03faa
TH
1643 // We want to list quizzes that are currently available, and which have a close date.
1644 // This is the same as what the lesson does, and the dabate is in MDL-10568.
6c58e198 1645 $now = time();
2a13e454 1646 foreach ($quizzes as $quiz) {
b5a16eb7 1647 if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
ac032e0e 1648 $str = '';
b5a16eb7 1649
25a03faa 1650 // Now provide more information depending on the uers's role.
c492a78e 1651 $context = context_module::instance($quiz->coursemodule);
b5a16eb7 1652 if (has_capability('mod/quiz:viewreports', $context)) {
25a03faa 1653 // For teacher-like people, show a summary of the number of student attempts.
9cf4a18b 1654 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
2a13e454 1655 // fields set to make the following call work.
ac032e0e
FM
1656 $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
1657
1658 } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student
25a03faa 1659 // For student-like people, tell them how many attempts they have made.
ac032e0e
FM
1660
1661 if (isset($USER->id)) {
1662 if ($attemptsinfo[$quiz->id]['hasfinished']) {
1663 // The student's last attempt is finished.
1664 continue;
1665 }
1666
1667 if ($attemptsinfo[$quiz->id]['count'] > 0) {
1668 $str .= '<div class="info">' .
1669 get_string('numattemptsmade', 'quiz', $attemptsinfo[$quiz->id]['count']) . '</div>';
1670 } else {
1671 $str .= '<div class="info">' . $strnoattempts . '</div>';
1672 }
1673
b5a16eb7 1674 } else {
1675 $str .= '<div class="info">' . $strnoattempts . '</div>';
1676 }
ac032e0e 1677
b5a16eb7 1678 } else {
25a03faa 1679 // For ayone else, there is no point listing this quiz, so stop processing.
b5a16eb7 1680 continue;
1681 }
1682
ac032e0e
FM
1683 // Give a link to the quiz, and the deadline.
1684 $html = '<div class="quiz overview">' .
1685 '<div class="name">' . $strquiz . ': <a ' .
1686 ($quiz->visible ? '' : ' class="dimmed"') .
1687 ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
1688 $quiz->coursemodule . '">' .
1689 $quiz->name . '</a></div>';
1690 $html .= '<div class="info">' . get_string('quizcloseson', 'quiz',
1691 userdate($quiz->timeclose)) . '</div>';
1692 $html .= $str;
1693 $html .= '</div>';
b5a16eb7 1694 if (empty($htmlarray[$quiz->course]['quiz'])) {
ac032e0e 1695 $htmlarray[$quiz->course]['quiz'] = $html;
b5a16eb7 1696 } else {
ac032e0e 1697 $htmlarray[$quiz->course]['quiz'] .= $html;
b5a16eb7 1698 }
1699 }
1700 }
1701}
6c58e198 1702
1703/**
25302dee 1704 * Return a textual summary of the number of attempts that have been made at a particular quiz,
cd300cf3 1705 * returns '' if no attempts have been made yet, unless $returnzero is passed as true.
8cc86111 1706 *
6c58e198 1707 * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
25a03faa
TH
1708 * @param object $cm the cm object. Only $cm->course, $cm->groupmode and
1709 * $cm->groupingid fields are used at the moment.
1710 * @param bool $returnzero if false (default), when no attempts have been
1711 * made '' is returned instead of 'Attempts: 0'.
2a13e454 1712 * @param int $currentgroup if there is a concept of current group where this method is being called
1713 * (e.g. a report) pass it in here. Default 0 which means no current group.
1714 * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
1715 * "Attemtps 123 (45 from this group)".
6c58e198 1716 */
2a13e454 1717function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
a49cb927 1718 global $DB, $USER;
9cf4a18b 1719 $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0));
6c58e198 1720 if ($numattempts || $returnzero) {
2a13e454 1721 if (groups_get_activity_groupmode($cm)) {
92701024 1722 $a = new stdClass();
2a13e454 1723 $a->total = $numattempts;
1724 if ($currentgroup) {
3f11d7f9 1725 $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' .
9cf4a18b 1726 '{quiz_attempts} qa JOIN ' .
1727 '{groups_members} gm ON qa.userid = gm.userid ' .
25a03faa
TH
1728 'WHERE quiz = ? AND preview = 0 AND groupid = ?',
1729 array($quiz->id, $currentgroup));
2a13e454 1730 return get_string('attemptsnumthisgroup', 'quiz', $a);
9cf4a18b 1731 } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
1732 list($usql, $params) = $DB->get_in_or_equal(array_keys($groups));
3f11d7f9 1733 $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' .
9cf4a18b 1734 '{quiz_attempts} qa JOIN ' .
1735 '{groups_members} gm ON qa.userid = gm.userid ' .
1736 'WHERE quiz = ? AND preview = 0 AND ' .
1737 "groupid $usql", array_merge(array($quiz->id), $params));
2a13e454 1738 return get_string('attemptsnumyourgroups', 'quiz', $a);
1739 }
1740 }
6c58e198 1741 return get_string('attemptsnum', 'quiz', $numattempts);
1742 }
1743 return '';
1744}
f432bebf 1745
4e781c7b 1746/**
a49cb927
TH
1747 * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
1748 * to the quiz reports.
1749 *
1750 * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
25a03faa
TH
1751 * @param object $cm the cm object. Only $cm->course, $cm->groupmode and
1752 * $cm->groupingid fields are used at the moment.
a49cb927 1753 * @param object $context the quiz context.
25a03faa
TH
1754 * @param bool $returnzero if false (default), when no attempts have been made
1755 * '' is returned instead of 'Attempts: 0'.
a49cb927
TH
1756 * @param int $currentgroup if there is a concept of current group where this method is being called
1757 * (e.g. a report) pass it in here. Default 0 which means no current group.
1758 * @return string HTML fragment for the link.
1759 */
25a03faa
TH
1760function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false,
1761 $currentgroup = 0) {
a49cb927
TH
1762 global $CFG;
1763 $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
1764 if (!$summary) {
1765 return '';
1766 }
1767
1768 require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1769 $url = new moodle_url('/mod/quiz/report.php', array(
1770 'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
1771 return html_writer::link($url, $summary);
1772}
1773
1774/**
4e781c7b 1775 * @param string $feature FEATURE_xx constant for requested feature
1776 * @return bool True if quiz supports feature
1777 */
1778function quiz_supports($feature) {
1779 switch($feature) {
455dc0de
FM
1780 case FEATURE_GROUPS: return true;
1781 case FEATURE_GROUPINGS: return true;
455dc0de
FM
1782 case FEATURE_MOD_INTRO: return true;
1783 case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
349c9303 1784 case FEATURE_COMPLETION_HAS_RULES: return true;
455dc0de 1785 case FEATURE_GRADE_HAS_GRADE: return true;
7e48778b 1786 case FEATURE_GRADE_OUTCOMES: return true;
455dc0de
FM
1787 case FEATURE_BACKUP_MOODLE2: return true;
1788 case FEATURE_SHOW_DESCRIPTION: return true;
1789 case FEATURE_CONTROLS_GRADE_VISIBILITY: return true;
b07ef13b 1790 case FEATURE_USES_QUESTIONS: return true;
42f103be 1791
49f6e5f4 1792 default: return null;
4e781c7b 1793 }
1794}
1795
f432bebf 1796/**
cca6e300 1797 * @return array all other caps used in module
f432bebf 1798 */
1799function quiz_get_extra_capabilities() {
d774e817 1800 global $CFG;
172238f4 1801 require_once($CFG->libdir . '/questionlib.php');
cca6e300 1802 $caps = question_get_all_capabilities();
1803 $caps[] = 'moodle/site:accessallgroups';
1804 return $caps;
f432bebf 1805}
55f599f0 1806
55f599f0 1807/**
1808 * This function extends the settings navigation block for the site.
1809 *
1810 * It is safe to rely on PAGE here as we will only ever be within the module
1811 * context when this is called
1812 *
0b29477b
SH
1813 * @param settings_navigation $settings
1814 * @param navigation_node $quiznode
ebd9ae65 1815 * @return void
55f599f0 1816 */
0b29477b
SH
1817function quiz_extend_settings_navigation($settings, $quiznode) {
1818 global $PAGE, $CFG;
55f599f0 1819
9e83f3d1
TH
1820 // Require {@link questionlib.php}
1821 // Included here as we only ever want to include this file if we really need to.
56ed242b 1822 require_once($CFG->libdir . '/questionlib.php');
55f599f0 1823
bc502c16
TH
1824 // We want to add these new nodes after the Edit settings node, and before the
1825 // Locally assigned roles node. Of course, both of those are controlled by capabilities.
1826 $keys = $quiznode->get_children_key_list();
1b8655ab 1827 $beforekey = null;
bc502c16 1828 $i = array_search('modedit', $keys);
1b8655ab 1829 if ($i === false and array_key_exists(0, $keys)) {
bc502c16
TH
1830 $beforekey = $keys[0];
1831 } else if (array_key_exists($i + 1, $keys)) {
1832 $beforekey = $keys[$i + 1];
bc502c16
TH
1833 }
1834
56ed242b 1835 if (has_capability('mod/quiz:manageoverrides', $PAGE->cm->context)) {
56ed242b 1836 $url = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$PAGE->cm->id));
bc502c16 1837 $node = navigation_node::create(get_string('groupoverrides', 'quiz'),
25a03faa 1838 new moodle_url($url, array('mode'=>'group')),
bc502c16
TH
1839 navigation_node::TYPE_SETTING, null, 'mod_quiz_groupoverrides');
1840 $quiznode->add_node($node, $beforekey);
1841
1842 $node = navigation_node::create(get_string('useroverrides', 'quiz'),
25a03faa 1843 new moodle_url($url, array('mode'=>'user')),
bc502c16
TH
1844 navigation_node::TYPE_SETTING, null, 'mod_quiz_useroverrides');
1845 $quiznode->add_node($node, $beforekey);
55f599f0 1846 }
56ed242b 1847
55f599f0 1848 if (has_capability('mod/quiz:manage', $PAGE->cm->context)) {
bc502c16
TH
1849 $node = navigation_node::create(get_string('editquiz', 'quiz'),
1850 new moodle_url('/mod/quiz/edit.php', array('cmid'=>$PAGE->cm->id)),
1851 navigation_node::TYPE_SETTING, null, 'mod_quiz_edit',
1852 new pix_icon('t/edit', ''));
1853 $quiznode->add_node($node, $beforekey);
55f599f0 1854 }
56ed242b
SH
1855
1856 if (has_capability('mod/quiz:preview', $PAGE->cm->context)) {
25a03faa
TH
1857 $url = new moodle_url('/mod/quiz/startattempt.php',
1858 array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey()));
bc502c16
TH
1859 $node = navigation_node::create(get_string('preview', 'quiz'), $url,
1860 navigation_node::TYPE_SETTING, null, 'mod_quiz_preview',
c3847443 1861 new pix_icon('i/preview', ''));
bc502c16 1862 $quiznode->add_node($node, $beforekey);
55f599f0 1863 }
56ed242b 1864
ebd9ae65
FM
1865 if (has_any_capability(array('mod/quiz:viewreports', 'mod/quiz:grade'), $PAGE->cm->context)) {
1866 require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1867 $reportlist = quiz_report_list($PAGE->cm->context);
1868
1869 $url = new moodle_url('/mod/quiz/report.php',
1870 array('id' => $PAGE->cm->id, 'mode' => reset($reportlist)));
1871 $reportnode = $quiznode->add_node(navigation_node::create(get_string('results', 'quiz'), $url,
1872 navigation_node::TYPE_SETTING,
1873 null, null, new pix_icon('i/report', '')), $beforekey);
1874
1875 foreach ($reportlist as $report) {
1876 $url = new moodle_url('/mod/quiz/report.php',
1877 array('id' => $PAGE->cm->id, 'mode' => $report));
1878 $reportnode->add_node(navigation_node::create(get_string($report, 'quiz_'.$report), $url,
1879 navigation_node::TYPE_SETTING,
1880 null, 'quiz_report_' . $report, new pix_icon('i/item', '')));
1881 }
1882 }
1883
2a8a78c3 1884 question_extend_settings_navigation($quiznode, $PAGE->cm->context)->trim_if_empty();
56ed242b 1885}
fe6ce234
DC
1886
1887/**
1888 * Serves the quiz files.
1889 *
d2b7803e
DC
1890 * @package mod_quiz
1891 * @category files
1892 * @param stdClass $course course object
1893 * @param stdClass $cm course module object
1894 * @param stdClass $context context object
1895 * @param string $filearea file area
1896 * @param array $args extra arguments
1897 * @param bool $forcedownload whether or not force download
261cbbac 1898 * @param array $options additional options affecting the file serving
fe6ce234
DC
1899 * @return bool false if file not found, does not return if found - justsend the file
1900 */
261cbbac 1901function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
fe6ce234
DC
1902 global $CFG, $DB;
1903
1904 if ($context->contextlevel != CONTEXT_MODULE) {
1905 return false;
1906 }
1907
1908 require_login($course, false, $cm);
1909
1910 if (!$quiz = $DB->get_record('quiz', array('id'=>$cm->instance))) {
1911 return false;
1912 }
1913
9e83f3d1 1914 // The 'intro' area is served by pluginfile.php.
fe6ce234
DC
1915 $fileareas = array('feedback');
1916 if (!in_array($filearea, $fileareas)) {
1917 return false;
1918 }
1919
1920 $feedbackid = (int)array_shift($args);
1921 if (!$feedback = $DB->get_record('quiz_feedback', array('id'=>$feedbackid))) {
1922 return false;
1923 }
1924
1925 $fs = get_file_storage();
1926 $relativepath = implode('/', $args);
1927 $fullpath = "/$context->id/mod_quiz/$filearea/$feedbackid/$relativepath";
1928 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1929 return false;
1930 }
261cbbac 1931 send_stored_file($file, 0, 0, true, $options);
fe6ce234
DC
1932}
1933
1934/**
1935 * Called via pluginfile.php -> question_pluginfile to serve files belonging to
1936 * a question in a question_attempt when that attempt is a quiz attempt.
1937 *
d2b7803e
DC
1938 * @package mod_quiz
1939 * @category files
1940 * @param stdClass $course course settings object
1941 * @param stdClass $context context object
fe6ce234
DC
1942 * @param string $component the name of the component we are serving files for.
1943 * @param string $filearea the name of the file area.
d2b7803e
DC
1944 * @param int $qubaid the attempt usage id.
1945 * @param int $slot the id of a question in this quiz attempt.
fe6ce234
DC
1946 * @param array $args the remaining bits of the file path.
1947 * @param bool $forcedownload whether the user must be forced to download the file.
261cbbac 1948 * @param array $options additional options affecting the file serving
fe6ce234
DC
1949 * @return bool false if file not found, does not return if found - justsend the file
1950 */
6b8b0b2e 1951function quiz_question_pluginfile($course, $context, $component,
261cbbac 1952 $filearea, $qubaid, $slot, $args, $forcedownload, array $options=array()) {
cef18275 1953 global $CFG;
fe6ce234
DC
1954 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
1955
56e82d99 1956 $attemptobj = quiz_attempt::create_from_usage_id($qubaid);
cdbea7ee 1957 require_login($attemptobj->get_course(), false, $attemptobj->get_cm());
fe6ce234
DC
1958
1959 if ($attemptobj->is_own_attempt() && !$attemptobj->is_finished()) {
1960 // In the middle of an attempt.
1961 if (!$attemptobj->is_preview_user()) {
1962 $attemptobj->require_capability('mod/quiz:attempt');
1963 }
1964 $isreviewing = false;
1965
1966 } else {
1967 // Reviewing an attempt.
1968 $attemptobj->check_review_capability();
1969 $isreviewing = true;
1970 }
1971
56e82d99 1972 if (!$attemptobj->check_file_access($slot, $isreviewing, $context->id,
fe6ce234
DC
1973 $component, $filearea, $args, $forcedownload)) {
1974 send_file_not_found();
1975 }
1976
1977 $fs = get_file_storage();
1978 $relativepath = implode('/', $args);
1979 $fullpath = "/$context->id/$component/$filearea/$relativepath";
1980 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1981 send_file_not_found();
1982 }
1983
261cbbac 1984 send_stored_file($file, 0, 0, $forcedownload, $options);
fe6ce234 1985}
b1627a92
DC
1986
1987/**
1988 * Return a list of page types
1989 * @param string $pagetype current page type
1990 * @param stdClass $parentcontext Block's parent context
1991 * @param stdClass $currentcontext Current context of block
1992 */
b38e2e28 1993function quiz_page_type_list($pagetype, $parentcontext, $currentcontext) {
346a32a7 1994 $module_pagetype = array(
f38965db
TH
1995 'mod-quiz-*' => get_string('page-mod-quiz-x', 'quiz'),
1996 'mod-quiz-view' => get_string('page-mod-quiz-view', 'quiz'),
1997 'mod-quiz-attempt' => get_string('page-mod-quiz-attempt', 'quiz'),
1998 'mod-quiz-summary' => get_string('page-mod-quiz-summary', 'quiz'),
1999 'mod-quiz-review' => get_string('page-mod-quiz-review', 'quiz'),
2000 'mod-quiz-edit' => get_string('page-mod-quiz-edit', 'quiz'),
2001 'mod-quiz-report' => get_string('page-mod-quiz-report', 'quiz'),
2002 );
b1627a92
DC
2003 return $module_pagetype;
2004}
33c8d37b
CF
2005
2006/**
2007 * @return the options for quiz navigation.
2008 */
2009function quiz_get_navigation_options() {
2010 return array(
2011 QUIZ_NAVMETHOD_FREE => get_string('navmethod_free', 'quiz'),
2012 QUIZ_NAVMETHOD_SEQ => get_string('navmethod_seq', 'quiz')
2013 );
2014}
349c9303 2015
349c9303
RM
2016/**
2017 * Obtains the automatic completion state for this quiz on any conditions
2018 * in quiz settings, such as if all attempts are used or a certain grade is achieved.
2019 *
2020 * @param object $course Course
2021 * @param object $cm Course-module
2022 * @param int $userid User ID
2023 * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)
2024 * @return bool True if completed, false if not. (If no conditions, then return
2025 * value depends on comparison type)
2026 */
2027function quiz_get_completion_state($course, $cm, $userid, $type) {
2028 global $DB;
2029 global $CFG;
2030
2031 $quiz = $DB->get_record('quiz', array('id' => $cm->instance), '*', MUST_EXIST);
2032 if (!$quiz->completionattemptsexhausted && !$quiz->completionpass) {
2033 return $type;
2034 }
2035
2036 // Check if the user has used up all attempts.
2037 if ($quiz->completionattemptsexhausted) {
2038 $attempts = quiz_get_user_attempts($quiz->id, $userid, 'finished', true);
2039 if ($attempts) {
2040 $lastfinishedattempt = end($attempts);
2041 $context = context_module::instance($cm->id);
2042 $quizobj = quiz::create($quiz->id, $userid);
2043 $accessmanager = new quiz_access_manager($quizobj, time(),
2044 has_capability('mod/quiz:ignoretimelimits', $context, $userid, false));
2045 if ($accessmanager->is_finished(count($attempts), $lastfinishedattempt)) {
2046 return true;
2047 }
2048 }
2049 }
2050
2051 // Check for passing grade.
2052 if ($quiz->completionpass) {
2053 require_once($CFG->libdir . '/gradelib.php');
2054 $item = grade_item::fetch(array('courseid' => $course->id, 'itemtype' => 'mod',
1245bfd5 2055 'itemmodule' => 'quiz', 'iteminstance' => $cm->instance, 'outcomeid' => null));
349c9303
RM
2056 if ($item) {
2057 $grades = grade_grade::fetch_users_grades($item, array($userid), false);
2058 if (!empty($grades[$userid])) {
2059 return $grades[$userid]->is_passed($item);
2060 }
2061 }
2062 }
2063 return false;
2064}
02a73d76
JL
2065
2066/**
2067 * Check if the module has any update that affects the current user since a given time.
2068 *
2069 * @param cm_info $cm course module data
2070 * @param int $from the time to check updates from
2071 * @param array $filter if we need to check only specific updates
2072 * @return stdClass an object with the different type of areas indicating if they were updated or not
2073 * @since Moodle 3.2
2074 */
2075function quiz_check_updates_since(cm_info $cm, $from, $filter = array()) {
2076 global $DB, $USER, $CFG;
2077 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
2078
2079 $updates = course_check_module_updates_since($cm, $from, array(), $filter);
2080
2081 // Check if questions were updated.
65b2669d 2082 $updates->questions = (object) array('updated' => false);
02a73d76
JL
2083 $quizobj = quiz::create($cm->instance, $USER->id);
2084 $quizobj->preload_questions();
2085 $quizobj->load_questions();
2086 $questionids = array_keys($quizobj->get_questions());
2087 if (!empty($questionids)) {
2088 list($questionsql, $params) = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED);
2089 $select = 'id ' . $questionsql . ' AND (timemodified > :time1 OR timecreated > :time2)';
2090 $params['time1'] = $from;
2091 $params['time2'] = $from;
8abfb7b1 2092 $questions = $DB->get_records_select('question', $select, $params, '', 'id');
65b2669d
JL
2093 if (!empty($questions)) {
2094 $updates->questions->updated = true;
2095 $updates->questions->itemids = array_keys($questions);
2096 }
02a73d76
JL
2097 }
2098
2099 // Check for new attempts or grades.
65b2669d
JL
2100 $updates->attempts = (object) array('updated' => false);
2101 $updates->grades = (object) array('updated' => false);
02a73d76
JL
2102 $select = 'quiz = ? AND userid = ? AND timemodified > ?';
2103 $params = array($cm->instance, $USER->id, $from);
65b2669d
JL
2104
2105 $attempts = $DB->get_records_select('quiz_attempts', $select, $params, '', 'id');
2106 if (!empty($attempts)) {
2107 $updates->attempts->updated = true;
2108 $updates->attempts->itemids = array_keys($attempts);
2109 }
2110 $grades = $DB->get_records_select('quiz_grades', $select, $params, '', 'id');
2111 if (!empty($grades)) {
2112 $updates->grades->updated = true;
2113 $updates->grades->itemids = array_keys($grades);
2114 }
02a73d76 2115
0a348387
JL
2116 // Now, teachers should see other students updates.
2117 if (has_capability('mod/quiz:viewreports', $cm->context)) {
2118 $select = 'quiz = ? AND timemodified > ?';
2119 $params = array($cm->instance, $from);
2120
2121 if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
2122 $groupusers = array_keys(groups_get_activity_shared_group_members($cm));
2123 if (empty($groupusers)) {
2124 return $updates;
2125 }
2126 list($insql, $inparams) = $DB->get_in_or_equal($groupusers);
2127 $select .= ' AND userid ' . $insql;
2128 $params = array_merge($params, $inparams);
2129 }
2130
2131 $updates->userattempts = (object) array('updated' => false);
2132 $attempts = $DB->get_records_select('quiz_attempts', $select, $params, '', 'id');
2133 if (!empty($attempts)) {
2134 $updates->userattempts->updated = true;
2135 $updates->userattempts->itemids = array_keys($attempts);
2136 }
2137
2138 $updates->usergrades = (object) array('updated' => false);
2139 $grades = $DB->get_records_select('quiz_grades', $select, $params, '', 'id');
2140 if (!empty($grades)) {
2141 $updates->usergrades->updated = true;
2142 $updates->usergrades->itemids = array_keys($grades);
2143 }
2144 }
02a73d76
JL
2145 return $updates;
2146}
2b931458
DW
2147
2148/**
2149 * Get icon mapping for font-awesome.
2150 */
2151function mod_quiz_get_fontawesome_icon_map() {
2152 return [
2153 'mod_quiz:navflagged' => 'fa-flag',
2154 ];
2155}
b3d7d175
MN
2156
2157/**
59391e80
MN
2158 * This function receives a calendar event and returns the action associated with it, or null if there is none.
2159 *
2160 * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
2161 * is not displayed on the block.
b3d7d175 2162 *
e1cd93ce 2163 * @param calendar_event $event
b3d7d175 2164 * @param \core_calendar\action_factory $factory
01f96180 2165 * @return \core_calendar\local\event\entities\action_interface|null
b3d7d175 2166 */
e1cd93ce 2167function mod_quiz_core_calendar_provide_event_action(calendar_event $event,
b3d7d175
MN
2168 \core_calendar\action_factory $factory) {
2169 global $CFG, $USER;
2170
2171 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
2172
2173 $cm = get_fast_modinfo($event->courseid)->instances['quiz'][$event->instance];
2174 $quizobj = quiz::create($cm->instance, $USER->id);
2175 $quiz = $quizobj->get_quiz();
2176
2177 // Check they have capabilities allowing them to view the quiz.
2178 if (!has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $quizobj->get_context())) {
2179 return null;
2180 }
2181
2182 quiz_update_effective_access($quiz, $USER->id);
2183
2184 // Check if quiz is closed, if so don't display it.
2185 if (!empty($quiz->timeclose) && $quiz->timeclose <= time()) {
2186 return null;
2187 }
2188
2189 $attempts = quiz_get_user_attempts($quizobj->get_quizid(), $USER->id);
2190 if (!empty($attempts)) {
2191 // The student's last attempt is finished.
2192 return null;
2193 }
2194
2195 $name = get_string('attemptquiznow', 'quiz');
2196 $url = new \moodle_url('/mod/quiz/view.php', [
2197 'id' => $cm->id
2198 ]);
2199 $itemcount = 1;
2200 $actionable = true;
2201
2202 // Check if the quiz is not currently actionable.
2203 if (!empty($quiz->timeopen) && $quiz->timeopen > time()) {
2204 $actionable = false;
2205 }
2206
2207 return $factory->create_instance(
2208 $name,
2209 $url,
2210 $itemcount,
2211 $actionable
2212 );
de66ac3c 2213}
b54bcddd
JD
2214
2215/**
2216 * Add a get_coursemodule_info function in case any quiz type wants to add 'extra' information
2217 * for the course (see resource).
2218 *
2219 * Given a course_module object, this function returns any "extra" information that may be needed
2220 * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php.
2221 *
2222 * @param stdClass $coursemodule The coursemodule object (record).
2223 * @return cached_cm_info An object on information that the courses
2224 * will know about (most noticeably, an icon).
2225 */
2226function quiz_get_coursemodule_info($coursemodule) {
2227 global $DB;
2228
2229 $dbparams = ['id' => $coursemodule->instance];
438c01d0 2230 $fields = 'id, name, intro, introformat, completionattemptsexhausted, completionpass';
b54bcddd
JD
2231 if (!$quiz = $DB->get_record('quiz', $dbparams, $fields)) {
2232 return false;
2233 }
2234
2235 $result = new cached_cm_info();
23e5104a 2236 $result->name = $quiz->name;
b54bcddd 2237
438c01d0
MG
2238 if ($coursemodule->showdescription) {
2239 // Convert intro to html. Do not filter cached version, filters run at display time.
2240 $result->content = format_module_intro('quiz', $quiz, $coursemodule->id, false);
2241 }
2242
b54bcddd
JD
2243 // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'.
2244 if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
2245 $result->customdata['customcompletionrules']['completionattemptsexhausted'] = $quiz->completionattemptsexhausted;
2246 $result->customdata['customcompletionrules']['completionpass'] = $quiz->completionpass;
2247 }
2248
2249 return $result;
2250}
2251
2252/**
2253 * Callback which returns human-readable strings describing the active completion custom rules for the module instance.
2254 *
7f53e8aa 2255 * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules']
b54bcddd
JD
2256 * @return array $descriptions the array of descriptions for the custom rules.
2257 */
2258function mod_quiz_get_completion_active_rule_descriptions($cm) {
2259 // Values will be present in cm_info, and we assume these are up to date.
7f53e8aa 2260 if (empty($cm->customdata['customcompletionrules'])
b54bcddd
JD
2261 || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) {
2262 return [];
2263 }
2264
2265 $descriptions = [];
2266 foreach ($cm->customdata['customcompletionrules'] as $key => $val) {
2267 switch ($key) {
2268 case 'completionattemptsexhausted':
2269 if (empty($val)) {
2270 continue;
2271 }
2272 $descriptions[] = get_string('completionattemptsexhausteddesc', 'quiz');
2273 break;
2274 case 'completionpass':
2275 if (empty($val)) {
2276 continue;
2277 }
2278 $descriptions[] = get_string('completionpassdesc', 'quiz', format_time($val));
2279 break;
2280 default:
2281 break;
2282 }
2283 }
2284 return $descriptions;
2285}
9aac9f70
RW
2286
2287/**
2288 * Returns the min and max values for the timestart property of a quiz
2289 * activity event.
2290 *
2291 * The min and max values will be the timeopen and timeclose properties
2292 * of the quiz, respectively, if they are set.
2293 *
2294 * If either value isn't set then null will be returned instead to
2295 * indicate that there is no cutoff for that value.
2296 *
6688ae2b
RW
2297 * If the vent has no valid timestart range then [false, false] will
2298 * be returned. This is the case for overriden events.
2299 *
9aac9f70
RW
2300 * A minimum and maximum cutoff return value will look like:
2301 * [
2302 * [1505704373, 'The date must be after this date'],
2303 * [1506741172, 'The date must be before this date']
2304 * ]
2305 *
478b1d19 2306 * @throws \moodle_exception
9aac9f70 2307 * @param \calendar_event $event The calendar event to get the time range for
478b1d19 2308 * @param stdClass $quiz The module instance to get the range from
9aac9f70
RW
2309 * @return array
2310 */
478b1d19 2311function mod_quiz_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $quiz) {
9aac9f70
RW
2312 global $CFG, $DB;
2313 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
2314
6688ae2b 2315 // Overrides do not have a valid timestart range.
9aac9f70 2316 if (quiz_is_overriden_calendar_event($event)) {
6688ae2b 2317 return [false, false];
9aac9f70
RW
2318 }
2319
9aac9f70
RW
2320 $mindate = null;
2321 $maxdate = null;
2322
2323 if ($event->eventtype == QUIZ_EVENT_TYPE_OPEN) {
2324 if (!empty($quiz->timeclose)) {
2325 $maxdate = [
2326 $quiz->timeclose,
2327 get_string('openafterclose', 'quiz')
2328 ];
2329 }
2330 } else if ($event->eventtype == QUIZ_EVENT_TYPE_CLOSE) {
2331 if (!empty($quiz->timeopen)) {
2332 $mindate = [
2333 $quiz->timeopen,
2334 get_string('closebeforeopen', 'quiz')
2335 ];
2336 }
2337 }
2338
2339 return [$mindate, $maxdate];
2340}
2341
9aac9f70
RW
2342/**
2343 * This function will update the quiz module according to the
2344 * event that has been modified.
2345 *
2346 * It will set the timeopen or timeclose value of the quiz instance
2347 * according to the type of event provided.
2348 *
2349 * @throws \moodle_exception
478b1d19
RW
2350 * @param \calendar_event $event A quiz activity calendar event
2351 * @param \stdClass $quiz A quiz activity instance
9aac9f70 2352 */
478b1d19 2353function mod_quiz_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $quiz) {
9aac9f70
RW
2354 global $CFG, $DB;
2355 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
2356
478b1d19
RW
2357 if (!in_array($event->eventtype, [QUIZ_EVENT_TYPE_OPEN, QUIZ_EVENT_TYPE_CLOSE])) {
2358 // This isn't an event that we care about so we can ignore it.
9aac9f70
RW
2359 return;
2360 }
2361
2362 $courseid = $event->courseid;
2363 $modulename = $event->modulename;
2364 $instanceid = $event->instance;
2365 $modified = false;
2366 $closedatechanged = false;
2367
2368 // Something weird going on. The event is for a different module so
2369 // we should ignore it.
2370 if ($modulename != 'quiz') {
2371 return;
2372 }
2373
478b1d19
RW
2374 if ($quiz->id != $instanceid) {
2375 // The provided quiz instance doesn't match the event so
2376 // there is nothing to do here.
2377 return;
2378 }
2379
2380 // We don't update the activity if it's an override event that has
2381 // been modified.
2382 if (quiz_is_overriden_calendar_event($event)) {
2383 return;
2384 }
2385
9aac9f70
RW
2386 $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
2387 $context = context_module::instance($coursemodule->id);
2388
2389 // The user does not have the capability to modify this activity.
2390 if (!has_capability('moodle/course:manageactivities', $context)) {
2391 return;
2392 }
2393
2394 if ($event->eventtype == QUIZ_EVENT_TYPE_OPEN) {
2395 // If the event is for the quiz activity opening then we should
2396 // set the start time of the quiz activity to be the new start
2397 // time of the event.
9aac9f70
RW
2398 if ($quiz->timeopen != $event->timestart) {
2399 $quiz->timeopen = $event->timestart;
2400 $modified = true;
2401 }
2402 } else if ($event->eventtype == QUIZ_EVENT_TYPE_CLOSE) {
2403 // If the event is for the quiz activity closing then we should
2404 // set the end time of the quiz activity to be the new start
2405 // time of the event.
9aac9f70
RW
2406 if ($quiz->timeclose != $event->timestart) {
2407 $quiz->timeclose = $event->timestart;
2408 $modified = true;
2409 $closedatechanged = true;
2410 }
2411 }
2412
2413 if ($modified) {
2414 $quiz->timemodified = time();
2415 $DB->update_record('quiz', $quiz);
2416
2417 if ($closedatechanged) {
2418 quiz_update_open_attempts(array('quizid' => $quiz->id));
2419 }
2420
2421 // Delete any previous preview attempts.
2422 quiz_delete_previews($quiz);
2423 quiz_update_events($quiz);
2424 $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
2425 $event->trigger();
2426 }
2427}
f33dc783
RW
2428
2429/**
f046b9a4 2430 * Generates the question bank in a fragment output. This allows
f33dc783
RW
2431 * the question bank to be displayed in a modal.
2432 *
2433 * The only expected argument provided in the $args array is
2434 * 'querystring'. The value should be the list of parameters
2435 * URL encoded and used to build the question bank page.
2436 *
2437 * The individual list of parameters expected can be found in
2438 * question_build_edit_resources.
2439 *
2440 * @param array $args The fragment arguments.
2441 * @return string The rendered mform fragment.
2442 */
2443function mod_quiz_output_fragment_quiz_question_bank($args) {
2444 global $CFG, $DB, $PAGE;
2445 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
2446 require_once($CFG->dirroot . '/question/editlib.php');
2447
2448 $querystring = preg_replace('/^\?/', '', $args['querystring']);
2449 $params = [];
2450 parse_str($querystring, $params);
2451
2452 // Build the required resources. The $params are all cleaned as
2453 // part of this process.
2454 list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
2455 question_build_edit_resources('editq', '/mod/quiz/edit.php', $params);
2456
2457 // Get the course object and related bits.
2458 $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
2459 require_capability('mod/quiz:manage', $contexts->lowest());
2460
2461 // Create quiz question bank view.
fc23bc85 2462 $questionbank = new mod_quiz\question\bank\custom_view($contexts, $thispageurl, $course, $cm, $quiz);
f33dc783
RW
2463 $questionbank->set_quiz_has_attempts(quiz_has_attempts($quiz->id));
2464
2465 // Output.
2466 $renderer = $PAGE->get_renderer('mod_quiz', 'edit');
2467 return $renderer->question_bank_contents($questionbank, $pagevars);
2468}
f046b9a4
RW
2469
2470/**
2471 * Generates the add random question in a fragment output. This allows the
2472 * form to be rendered in javascript, for example inside a modal.
2473 *
2474 * The required arguments as keys in the $args array are:
2475 * cat {string} The category and category context ids comma separated.
2476 * addonpage {int} The page id to add this question to.
2477 * returnurl {string} URL to return to after form submission.
2478 * cmid {int} The course module id the questions are being added to.
2479 *
2480 * @param array $args The fragment arguments.
2481 * @return string The rendered mform fragment.
2482 */
2483function mod_quiz_output_fragment_add_random_question_form($args) {
2484 global $CFG;
2485 require_once($CFG->dirroot . '/mod/quiz/addrandomform.php');
2486
2487 $contexts = new \question_edit_contexts($args['context']);
2488 $formoptions = [
2489 'contexts' => $contexts,
2490 'cat' => $args['cat']
2491 ];
2492 $formdata = [
2493 'category' => $args['cat'],
2494 'addonpage' => $args['addonpage'],
2495 'returnurl' => $args['returnurl'],
2496 'cmid' => $args['cmid']
2497 ];
2498
2499 $form = new quiz_add_random_form(
2500 new \moodle_url('/mod/quiz/addrandom.php'),
2501 $formoptions,
2502 'post',
2503 '',
2504 null,
2505 true,
2506 $formdata
2507 );
2508 $form->set_data($formdata);
2509
2510 return $form->render();
2511}