Commit | Line | Data |
---|---|---|
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 | * | |
ba643847 | 23 | * @package mod |
25302dee | 24 | * @subpackage quiz |
ba643847 TH |
25 | * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} |
26 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
84e628a0 | 27 | */ |
730fd187 | 28 | |
25302dee | 29 | |
a17b297d TH |
30 | defined('MOODLE_INTERNAL') || die(); |
31 | ||
84e628a0 | 32 | require_once($CFG->libdir . '/eventslib.php'); |
f81a8247 | 33 | require_once($CFG->dirroot . '/calendar/lib.php'); |
8966a111 | 34 | |
75cd257b | 35 | |
e2249afe | 36 | /**#@+ |
25302dee | 37 | * Option controlling what options are offered on the quiz settings form. |
e2249afe | 38 | */ |
84e628a0 | 39 | define('QUIZ_MAX_ATTEMPT_OPTION', 10); |
40 | define('QUIZ_MAX_QPP_OPTION', 50); | |
41 | define('QUIZ_MAX_DECIMAL_OPTION', 5); | |
42 | define('QUIZ_MAX_Q_DECIMAL_OPTION', 7); | |
75cd257b | 43 | /**#@-*/ |
44 | ||
4344c5d5 TH |
45 | /**#@+ |
46 | * Options determining how the grades from individual attempts are combined to give | |
47 | * the overall grade for a user | |
48 | */ | |
49 | define('QUIZ_GRADEHIGHEST', '1'); | |
50 | define('QUIZ_GRADEAVERAGE', '2'); | |
51 | define('QUIZ_ATTEMPTFIRST', '3'); | |
52 | define('QUIZ_ATTEMPTLAST', '4'); | |
53 | /**#@-*/ | |
54 | ||
75cd257b | 55 | /** |
9e83f3d1 | 56 | * @var int If start and end date for the quiz are more than this many seconds apart |
75cd257b | 57 | * they will be represented by two separate events in the calendar |
58 | */ | |
9e83f3d1 | 59 | define('QUIZ_MAX_EVENT_LENGTH', 5*24*60*60); // 5 days. |
ee1fb969 | 60 | |
33c8d37b CF |
61 | /**#@+ |
62 | * Options for navigation method within quizzes. | |
63 | */ | |
64 | define('QUIZ_NAVMETHOD_FREE', 'free'); | |
65 | define('QUIZ_NAVMETHOD_SEQ', 'sequential'); | |
66 | /**#@-*/ | |
67 | ||
920b93d1 | 68 | /** |
69 | * Given an object containing all the necessary data, | |
7cac0c4b | 70 | * (defined by the form in mod_form.php) this function |
920b93d1 | 71 | * will create a new instance and return the id number |
72 | * of the new instance. | |
a23f0aaf | 73 | * |
920b93d1 | 74 | * @param object $quiz the data that came from the form. |
212b7b8c | 75 | * @return mixed the id of the new instance on success, |
76 | * false or a string error message on failure. | |
920b93d1 | 77 | */ |
730fd187 | 78 | function quiz_add_instance($quiz) { |
c18269c7 | 79 | global $DB; |
fe6ce234 | 80 | $cmid = $quiz->coursemodule; |
730fd187 | 81 | |
920b93d1 | 82 | // Process the options from the form. |
83 | $quiz->created = time(); | |
bc569413 | 84 | $quiz->questions = ''; |
212b7b8c | 85 | $result = quiz_process_options($quiz); |
86 | if ($result && is_string($result)) { | |
87 | return $result; | |
88 | } | |
6f797013 | 89 | |
920b93d1 | 90 | // Try to store it in the database. |
eeab18f0 | 91 | $quiz->id = $DB->insert_record('quiz', $quiz); |
7bd1aa1d | 92 | |
920b93d1 | 93 | // Do the processing required after an add or an update. |
94 | quiz_after_add_or_update($quiz); | |
a23f0aaf | 95 | |
7bd1aa1d | 96 | return $quiz->id; |
730fd187 | 97 | } |
98 | ||
920b93d1 | 99 | /** |
100 | * Given an object containing all the necessary data, | |
7cac0c4b | 101 | * (defined by the form in mod_form.php) this function |
920b93d1 | 102 | * will update an existing instance with new data. |
a23f0aaf | 103 | * |
920b93d1 | 104 | * @param object $quiz the data that came from the form. |
212b7b8c | 105 | * @return mixed true on success, false or a string error message on failure. |
920b93d1 | 106 | */ |
eeab18f0 | 107 | function quiz_update_instance($quiz, $mform) { |
108 | global $CFG, $DB; | |
730fd187 | 109 | |
920b93d1 | 110 | // Process the options from the form. |
212b7b8c | 111 | $result = quiz_process_options($quiz); |
112 | if ($result && is_string($result)) { | |
113 | return $result; | |
114 | } | |
ee1fb969 | 115 | |
f2557823 | 116 | $oldquiz = $DB->get_record('quiz', array('id' => $quiz->instance)); |
25302dee | 117 | |
eeab18f0 | 118 | // Repaginate, if asked to. |
119 | if (!$quiz->shufflequestions && !empty($quiz->repaginatenow)) { | |
120 | require_once($CFG->dirroot . '/mod/quiz/locallib.php'); | |
7bf53cf2 TH |
121 | $quiz->questions = quiz_repaginate(quiz_clean_layout($oldquiz->questions, true), |
122 | $quiz->questionsperpage); | |
eeab18f0 | 123 | } |
124 | unset($quiz->repaginatenow); | |
125 | ||
920b93d1 | 126 | // Update the database. |
730fd187 | 127 | $quiz->id = $quiz->instance; |
eeab18f0 | 128 | $DB->update_record('quiz', $quiz); |
730fd187 | 129 | |
920b93d1 | 130 | // Do the processing required after an add or an update. |
131 | quiz_after_add_or_update($quiz); | |
ee1fb969 | 132 | |
25302dee TH |
133 | if ($oldquiz->grademethod != $quiz->grademethod) { |
134 | require_once($CFG->dirroot . '/mod/quiz/locallib.php'); | |
135 | $quiz->sumgrades = $oldquiz->sumgrades; | |
136 | $quiz->grade = $oldquiz->grade; | |
137 | quiz_update_all_final_grades($quiz); | |
138 | quiz_update_grades($quiz); | |
139 | } | |
140 | ||
9e83f3d1 | 141 | // Delete any previous preview attempts. |
53004e48 | 142 | quiz_delete_previews($quiz); |
d2f308c0 | 143 | |
7bd1aa1d | 144 | return true; |
730fd187 | 145 | } |
146 | ||
8cc86111 | 147 | /** |
148 | * Given an ID of an instance of this module, | |
149 | * this function will permanently delete the instance | |
150 | * and any data that depends on it. | |
151 | * | |
25302dee | 152 | * @param int $id the id of the quiz to delete. |
f7970e3c | 153 | * @return bool success or failure. |
8cc86111 | 154 | */ |
730fd187 | 155 | function quiz_delete_instance($id) { |
c18269c7 | 156 | global $DB; |
730fd187 | 157 | |
25302dee | 158 | $quiz = $DB->get_record('quiz', array('id' => $id), '*', MUST_EXIST); |
730fd187 | 159 | |
53004e48 | 160 | quiz_delete_all_attempts($quiz); |
990650f9 | 161 | quiz_delete_all_overrides($quiz); |
730fd187 | 162 | |
53004e48 | 163 | $DB->delete_records('quiz_question_instances', array('quiz' => $quiz->id)); |
164 | $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id)); | |
730fd187 | 165 | |
53004e48 | 166 | $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id)); |
25a03faa | 167 | foreach ($events as $event) { |
f81a8247 SH |
168 | $event = calendar_event::load($event); |
169 | $event->delete(); | |
b2a3cd2d | 170 | } |
171 | ||
d6dd2108 | 172 | quiz_grade_item_delete($quiz); |
53004e48 | 173 | $DB->delete_records('quiz', array('id' => $quiz->id)); |
d6dd2108 | 174 | |
53004e48 | 175 | return true; |
176 | } | |
177 | ||
990650f9 TH |
178 | /** |
179 | * Deletes a quiz override from the database and clears any corresponding calendar events | |
180 | * | |
181 | * @param object $quiz The quiz object. | |
f7970e3c | 182 | * @param int $overrideid The id of the override being deleted |
990650f9 TH |
183 | * @return bool true on success |
184 | */ | |
185 | function quiz_delete_override($quiz, $overrideid) { | |
186 | global $DB; | |
187 | ||
25302dee | 188 | $override = $DB->get_record('quiz_overrides', array('id' => $overrideid), '*', MUST_EXIST); |
990650f9 | 189 | |
9e83f3d1 | 190 | // Delete the events. |
25302dee TH |
191 | $events = $DB->get_records('event', array('modulename' => 'quiz', |
192 | 'instance' => $quiz->id, 'groupid' => (int)$override->groupid, | |
193 | 'userid' => (int)$override->userid)); | |
194 | foreach ($events as $event) { | |
990650f9 TH |
195 | $eventold = calendar_event::load($event); |
196 | $eventold->delete(); | |
197 | } | |
198 | ||
199 | $DB->delete_records('quiz_overrides', array('id' => $overrideid)); | |
200 | return true; | |
201 | } | |
202 | ||
203 | /** | |
204 | * Deletes all quiz overrides from the database and clears any corresponding calendar events | |
205 | * | |
206 | * @param object $quiz The quiz object. | |
207 | */ | |
208 | function quiz_delete_all_overrides($quiz) { | |
209 | global $DB; | |
210 | ||
211 | $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id'); | |
212 | foreach ($overrides as $override) { | |
213 | quiz_delete_override($quiz, $override->id); | |
214 | } | |
215 | } | |
216 | ||
217 | /** | |
218 | * Updates a quiz object with override information for a user. | |
219 | * | |
220 | * Algorithm: For each quiz setting, if there is a matching user-specific override, | |
221 | * then use that otherwise, if there are group-specific overrides, return the most | |
222 | * lenient combination of them. If neither applies, leave the quiz setting unchanged. | |
223 | * | |
224 | * Special case: if there is more than one password that applies to the user, then | |
225 | * quiz->extrapasswords will contain an array of strings giving the remaining | |
226 | * passwords. | |
227 | * | |
228 | * @param object $quiz The quiz object. | |
f7970e3c | 229 | * @param int $userid The userid. |
990650f9 TH |
230 | * @return object $quiz The updated quiz object. |
231 | */ | |
232 | function quiz_update_effective_access($quiz, $userid) { | |
233 | global $DB; | |
234 | ||
9e83f3d1 | 235 | // Check for user override. |
990650f9 TH |
236 | $override = $DB->get_record('quiz_overrides', array('quiz' => $quiz->id, 'userid' => $userid)); |
237 | ||
238 | if (!$override) { | |
6bdfef5d | 239 | $override = new stdClass(); |
990650f9 TH |
240 | $override->timeopen = null; |
241 | $override->timeclose = null; | |
242 | $override->timelimit = null; | |
243 | $override->attempts = null; | |
244 | $override->password = null; | |
245 | } | |
246 | ||
9e83f3d1 | 247 | // Check for group overrides. |
990650f9 | 248 | $groupings = groups_get_user_groups($quiz->course, $userid); |
990650f9 | 249 | |
7bc488dc | 250 | if (!empty($groupings[0])) { |
9e83f3d1 | 251 | // Select all overrides that apply to the User's groups. |
7bc488dc | 252 | list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0])); |
990650f9 TH |
253 | $sql = "SELECT * FROM {quiz_overrides} |
254 | WHERE groupid $extra AND quiz = ?"; | |
ac250ad5 | 255 | $params[] = $quiz->id; |
990650f9 TH |
256 | $records = $DB->get_records_sql($sql, $params); |
257 | ||
9e83f3d1 | 258 | // Combine the overrides. |
990650f9 TH |
259 | $opens = array(); |
260 | $closes = array(); | |
261 | $limits = array(); | |
262 | $attempts = array(); | |
263 | $passwords = array(); | |
264 | ||
265 | foreach ($records as $gpoverride) { | |
266 | if (isset($gpoverride->timeopen)) { | |
267 | $opens[] = $gpoverride->timeopen; | |
268 | } | |
269 | if (isset($gpoverride->timeclose)) { | |
270 | $closes[] = $gpoverride->timeclose; | |
271 | } | |
272 | if (isset($gpoverride->timelimit)) { | |
273 | $limits[] = $gpoverride->timelimit; | |
274 | } | |
275 | if (isset($gpoverride->attempts)) { | |
276 | $attempts[] = $gpoverride->attempts; | |
277 | } | |
278 | if (isset($gpoverride->password)) { | |
279 | $passwords[] = $gpoverride->password; | |
280 | } | |
281 | } | |
9e83f3d1 | 282 | // If there is a user override for a setting, ignore the group override. |
990650f9 | 283 | if (is_null($override->timeopen) && count($opens)) { |
25302dee | 284 | $override->timeopen = min($opens); |
990650f9 TH |
285 | } |
286 | if (is_null($override->timeclose) && count($closes)) { | |
25302dee | 287 | $override->timeclose = max($closes); |
990650f9 TH |
288 | } |
289 | if (is_null($override->timelimit) && count($limits)) { | |
25302dee | 290 | $override->timelimit = max($limits); |
990650f9 TH |
291 | } |
292 | if (is_null($override->attempts) && count($attempts)) { | |
25302dee | 293 | $override->attempts = max($attempts); |
990650f9 TH |
294 | } |
295 | if (is_null($override->password) && count($passwords)) { | |
25302dee | 296 | $override->password = array_shift($passwords); |
990650f9 | 297 | if (count($passwords)) { |
25302dee | 298 | $override->extrapasswords = $passwords; |
990650f9 TH |
299 | } |
300 | } | |
301 | ||
302 | } | |
303 | ||
9e83f3d1 | 304 | // Merge with quiz defaults. |
25302dee | 305 | $keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password', 'extrapasswords'); |
990650f9 TH |
306 | foreach ($keys as $key) { |
307 | if (isset($override->{$key})) { | |
308 | $quiz->{$key} = $override->{$key}; | |
309 | } | |
310 | } | |
311 | ||
312 | return $quiz; | |
313 | } | |
314 | ||
53004e48 | 315 | /** |
316 | * Delete all the attempts belonging to a quiz. | |
8cc86111 | 317 | * |
8cc86111 | 318 | * @param object $quiz The quiz object. |
53004e48 | 319 | */ |
320 | function quiz_delete_all_attempts($quiz) { | |
321 | global $CFG, $DB; | |
6b5f24d3 TH |
322 | require_once($CFG->dirroot . '/mod/quiz/locallib.php'); |
323 | question_engine::delete_questions_usage_by_activities(new qubaids_for_quiz($quiz->id)); | |
53004e48 | 324 | $DB->delete_records('quiz_attempts', array('quiz' => $quiz->id)); |
325 | $DB->delete_records('quiz_grades', array('quiz' => $quiz->id)); | |
730fd187 | 326 | } |
327 | ||
25302dee TH |
328 | /** |
329 | * Get the best current grade for a particular user in a quiz. | |
330 | * | |
331 | * @param object $quiz the quiz settings. | |
f7970e3c | 332 | * @param int $userid the id of the user. |
25a03faa | 333 | * @return float the user's current grade for this quiz, or null if this user does |
25302dee TH |
334 | * not have a grade on this quiz. |
335 | */ | |
336 | function quiz_get_best_grade($quiz, $userid) { | |
337 | global $DB; | |
25a03faa TH |
338 | $grade = $DB->get_field('quiz_grades', 'grade', |
339 | array('quiz' => $quiz->id, 'userid' => $userid)); | |
25302dee | 340 | |
b2607ccc | 341 | // Need to detect errors/no result, without catching 0 grades. |
25302dee TH |
342 | if ($grade === false) { |
343 | return null; | |
344 | } | |
345 | ||
346 | return $grade + 0; // Convert to number. | |
347 | } | |
348 | ||
349 | /** | |
350 | * Is this a graded quiz? If this method returns true, you can assume that | |
351 | * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to | |
352 | * divide by them). | |
353 | * | |
354 | * @param object $quiz a row from the quiz table. | |
f7970e3c | 355 | * @return bool whether this is a graded quiz. |
25302dee TH |
356 | */ |
357 | function quiz_has_grades($quiz) { | |
358 | return $quiz->grade >= 0.000005 && $quiz->sumgrades >= 0.000005; | |
359 | } | |
360 | ||
8cc86111 | 361 | /** |
362 | * Return a small object with summary information about what a | |
363 | * user has done with a given particular instance of this module | |
364 | * Used for user activity reports. | |
365 | * $return->time = the time they did it | |
366 | * $return->info = a short text description | |
367 | * | |
8cc86111 | 368 | * @param object $course |
369 | * @param object $user | |
370 | * @param object $mod | |
371 | * @param object $quiz | |
372 | * @return object|null | |
373 | */ | |
730fd187 | 374 | function quiz_user_outline($course, $user, $mod, $quiz) { |
1a96363a NC |
375 | global $DB, $CFG; |
376 | require_once("$CFG->libdir/gradelib.php"); | |
377 | $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id); | |
378 | ||
379 | if (empty($grades->items[0]->grades)) { | |
380 | return null; | |
381 | } else { | |
382 | $grade = reset($grades->items[0]->grades); | |
5ecfab51 | 383 | } |
384 | ||
0ff4bd08 | 385 | $result = new stdClass(); |
1a96363a | 386 | $result->info = get_string('grade') . ': ' . $grade->str_long_grade; |
4433f871 | 387 | |
9e83f3d1 TH |
388 | // Datesubmitted == time created. dategraded == time modified or time overridden |
389 | // if grade was last modified by the user themselves use date graded. Otherwise use | |
390 | // date submitted. | |
391 | // TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704. | |
4433f871 AD |
392 | if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) { |
393 | $result->time = $grade->dategraded; | |
394 | } else { | |
395 | $result->time = $grade->datesubmitted; | |
396 | } | |
397 | ||
5ecfab51 | 398 | return $result; |
1a96363a | 399 | } |
730fd187 | 400 | |
8cc86111 | 401 | /** |
402 | * Print a detailed representation of what a user has done with | |
403 | * a given particular instance of this module, for user activity reports. | |
404 | * | |
8cc86111 | 405 | * @param object $course |
406 | * @param object $user | |
407 | * @param object $mod | |
408 | * @param object $quiz | |
409 | * @return bool | |
410 | */ | |
730fd187 | 411 | function quiz_user_complete($course, $user, $mod, $quiz) { |
1a14a14b | 412 | global $DB, $CFG, $OUTPUT; |
be18f589 TH |
413 | require_once($CFG->libdir . '/gradelib.php'); |
414 | require_once($CFG->libdir . '/mod/quiz/locallib.php'); | |
25302dee | 415 | |
1a96363a NC |
416 | $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id); |
417 | if (!empty($grades->items[0]->grades)) { | |
418 | $grade = reset($grades->items[0]->grades); | |
419 | echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade); | |
420 | if ($grade->str_feedback) { | |
421 | echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback); | |
422 | } | |
423 | } | |
730fd187 | 424 | |
25a03faa TH |
425 | if ($attempts = $DB->get_records('quiz_attempts', |
426 | array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) { | |
ee1fb969 | 427 | foreach ($attempts as $attempt) { |
9378c99b | 428 | echo get_string('attempt', 'quiz', $attempt->attempt) . ': '; |
be18f589 TH |
429 | if ($attempt->state != quiz_attempt::FINISHED) { |
430 | echo quiz_attempt_state_name($attempt->state); | |
ee1fb969 | 431 | } else { |
25a03faa TH |
432 | echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' . |
433 | quiz_format_grade($quiz, $quiz->sumgrades); | |
ee1fb969 | 434 | } |
435 | echo ' - '.userdate($attempt->timemodified).'<br />'; | |
436 | } | |
437 | } else { | |
25a03faa | 438 | print_string('noattempts', 'quiz'); |
ee1fb969 | 439 | } |
440 | ||
730fd187 | 441 | return true; |
442 | } | |
443 | ||
8cc86111 | 444 | /** |
c2f5e2ab | 445 | * Quiz periodic clean-up tasks. |
8cc86111 | 446 | */ |
be0ba083 | 447 | function quiz_cron() { |
be18f589 | 448 | global $CFG; |
c2f5e2ab | 449 | |
e1bf1619 TH |
450 | // Since the quiz specifies $module->cron = 60, so that the subplugins can |
451 | // have frequent cron if they need it, we now need to do our own scheduling. | |
452 | $quizconfig = get_config('quiz'); | |
453 | if (!isset($quizconfig->overduelastrun)) { | |
454 | $quizconfig->overduelastrun = 0; | |
455 | $quizconfig->overduedoneto = 0; | |
456 | } | |
457 | ||
458 | $timenow = time(); | |
459 | if ($timenow > $quizconfig->overduelastrun + 3600) { | |
460 | require_once($CFG->dirroot . '/mod/quiz/cronlib.php'); | |
461 | $overduehander = new mod_quiz_overdue_attempt_updater(); | |
462 | ||
463 | $processto = $timenow - $quizconfig->graceperiodmin; | |
464 | $overduehander->update_overdue_attempts($timenow, $quizconfig->overduedoneto, $processto); | |
465 | ||
466 | set_config('overduelastrun', $timenow, 'quiz'); | |
467 | set_config('overduedoneto', $processto, 'quiz'); | |
468 | } | |
469 | ||
c2f5e2ab TH |
470 | // Run cron for our sub-plugin types. |
471 | cron_execute_plugin_type('quiz', 'quiz reports'); | |
472 | cron_execute_plugin_type('quizaccess', 'quiz access rules'); | |
7b0f4a37 DM |
473 | |
474 | return true; | |
730fd187 | 475 | } |
476 | ||
b5a16eb7 | 477 | /** |
f7970e3c TH |
478 | * @param int $quizid the quiz id. |
479 | * @param int $userid the userid. | |
b5a16eb7 | 480 | * @param string $status 'all', 'finished' or 'unfinished' to control |
8cc86111 | 481 | * @param bool $includepreviews |
25a03faa TH |
482 | * @return an array of all the user's attempts at this quiz. Returns an empty |
483 | * array if there are none. | |
b5a16eb7 | 484 | */ |
25302dee | 485 | function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) { |
36e48cb8 TH |
486 | global $DB, $CFG; |
487 | // TODO MDL-33071 it is very annoying to have to included all of locallib.php | |
488 | // just to get the quiz_attempt::FINISHED constants, but I will try to sort | |
489 | // that out properly for Moodle 2.4. For now, I will just do a quick fix for | |
490 | // MDL-33048. | |
491 | require_once($CFG->dirroot . '/mod/quiz/locallib.php'); | |
34b7d838 TH |
492 | |
493 | $params = array(); | |
494 | switch ($status) { | |
495 | case 'all': | |
496 | $statuscondition = ''; | |
497 | break; | |
498 | ||
499 | case 'finished': | |
500 | $statuscondition = ' AND state IN (:state1, :state2)'; | |
501 | $params['state1'] = quiz_attempt::FINISHED; | |
502 | $params['state2'] = quiz_attempt::ABANDONED; | |
503 | break; | |
504 | ||
505 | case 'unfinished': | |
506 | $statuscondition = ' AND state IN (:state1, :state2)'; | |
507 | $params['state1'] = quiz_attempt::IN_PROGRESS; | |
508 | $params['state2'] = quiz_attempt::OVERDUE; | |
509 | break; | |
510 | } | |
511 | ||
b5a16eb7 | 512 | $previewclause = ''; |
513 | if (!$includepreviews) { | |
514 | $previewclause = ' AND preview = 0'; | |
515 | } | |
34b7d838 TH |
516 | |
517 | $params['quizid'] = $quizid; | |
518 | $params['userid'] = $userid; | |
25302dee | 519 | return $DB->get_records_select('quiz_attempts', |
34b7d838 TH |
520 | 'quiz = :quizid AND userid = :userid' . $previewclause . $statuscondition, |
521 | $params, 'attempt ASC'); | |
b5a16eb7 | 522 | } |
858deff0 | 523 | |
d6dd2108 | 524 | /** |
525 | * Return grade for given user or all users. | |
526 | * | |
527 | * @param int $quizid id of quiz | |
528 | * @param int $userid optional user id, 0 means all users | |
f88fb62c | 529 | * @return array array of grades, false if none. These are raw grades. They should |
530 | * be processed with quiz_format_grade for display. | |
d6dd2108 | 531 | */ |
25302dee | 532 | function quiz_get_user_grades($quiz, $userid = 0) { |
9cf4a18b | 533 | global $CFG, $DB; |
d6dd2108 | 534 | |
9cf4a18b | 535 | $params = array($quiz->id); |
25302dee | 536 | $usertest = ''; |
9cf4a18b | 537 | if ($userid) { |
538 | $params[] = $userid; | |
25302dee TH |
539 | $usertest = 'AND u.id = ?'; |
540 | } | |
8f37f7fb | 541 | return $DB->get_records_sql(" |
25302dee TH |
542 | SELECT |
543 | u.id, | |
544 | u.id AS userid, | |
545 | qg.grade AS rawgrade, | |
546 | qg.timemodified AS dategraded, | |
547 | MAX(qa.timefinish) AS datesubmitted | |
548 | ||
549 | FROM {user} u | |
550 | JOIN {quiz_grades} qg ON u.id = qg.userid | |
551 | JOIN {quiz_attempts} qa ON qa.quiz = qg.quiz AND qa.userid = u.id | |
552 | ||
8f37f7fb TH |
553 | WHERE qg.quiz = ? |
554 | $usertest | |
555 | GROUP BY u.id, qg.grade, qg.timemodified", $params); | |
d6dd2108 | 556 | } |
557 | ||
f88fb62c | 558 | /** |
559 | * Round a grade to to the correct number of decimal places, and format it for display. | |
560 | * | |
561 | * @param object $quiz The quiz table row, only $quiz->decimalpoints is used. | |
562 | * @param float $grade The grade to round. | |
8cc86111 | 563 | * @return float |
f88fb62c | 564 | */ |
565 | function quiz_format_grade($quiz, $grade) { | |
25302dee TH |
566 | if (is_null($grade)) { |
567 | return get_string('notyetgraded', 'quiz'); | |
568 | } | |
f88fb62c | 569 | return format_float($grade, $quiz->decimalpoints); |
570 | } | |
571 | ||
84e628a0 | 572 | /** |
573 | * Round a grade to to the correct number of decimal places, and format it for display. | |
574 | * | |
575 | * @param object $quiz The quiz table row, only $quiz->decimalpoints is used. | |
576 | * @param float $grade The grade to round. | |
8cc86111 | 577 | * @return float |
84e628a0 | 578 | */ |
579 | function quiz_format_question_grade($quiz, $grade) { | |
25302dee TH |
580 | if (empty($quiz->questiondecimalpoints)) { |
581 | $quiz->questiondecimalpoints = -1; | |
582 | } | |
84e628a0 | 583 | if ($quiz->questiondecimalpoints == -1) { |
584 | return format_float($grade, $quiz->decimalpoints); | |
585 | } else { | |
586 | return format_float($grade, $quiz->questiondecimalpoints); | |
587 | } | |
588 | } | |
589 | ||
d6dd2108 | 590 | /** |
591 | * Update grades in central gradebook | |
592 | * | |
a153c9f2 | 593 | * @category grade |
25302dee TH |
594 | * @param object $quiz the quiz settings. |
595 | * @param int $userid specific user only, 0 means all users. | |
a153c9f2 | 596 | * @param bool $nullifnone If a single user is specified and $nullifnone is true a grade item with a null rawgrade will be inserted |
d6dd2108 | 597 | */ |
25302dee | 598 | function quiz_update_grades($quiz, $userid = 0, $nullifnone = true) { |
9cf4a18b | 599 | global $CFG, $DB; |
775f811a | 600 | require_once($CFG->libdir.'/gradelib.php'); |
ed1daaa9 | 601 | |
775f811a | 602 | if ($quiz->grade == 0) { |
603 | quiz_grade_item_update($quiz); | |
d6dd2108 | 604 | |
775f811a | 605 | } else if ($grades = quiz_get_user_grades($quiz, $userid)) { |
606 | quiz_grade_item_update($quiz, $grades); | |
eafb9d9e | 607 | |
25302dee | 608 | } else if ($userid && $nullifnone) { |
39790bd8 | 609 | $grade = new stdClass(); |
25302dee TH |
610 | $grade->userid = $userid; |
611 | $grade->rawgrade = null; | |
775f811a | 612 | quiz_grade_item_update($quiz, $grade); |
d6dd2108 | 613 | |
614 | } else { | |
775f811a | 615 | quiz_grade_item_update($quiz); |
616 | } | |
617 | } | |
3b1d5cc4 | 618 | |
775f811a | 619 | /** |
620 | * Update all grades in gradebook. | |
621 | */ | |
622 | function quiz_upgrade_grades() { | |
623 | global $DB; | |
624 | ||
625 | $sql = "SELECT COUNT('x') | |
626 | FROM {quiz} a, {course_modules} cm, {modules} m | |
627 | WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id"; | |
628 | $count = $DB->count_records_sql($sql); | |
629 | ||
630 | $sql = "SELECT a.*, cm.idnumber AS cmidnumber, a.course AS courseid | |
631 | FROM {quiz} a, {course_modules} cm, {modules} m | |
632 | WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id"; | |
3f2efc20 EL |
633 | $rs = $DB->get_recordset_sql($sql); |
634 | if ($rs->valid()) { | |
775f811a | 635 | $pbar = new progress_bar('quizupgradegrades', 500, true); |
636 | $i=0; | |
637 | foreach ($rs as $quiz) { | |
638 | $i++; | |
9e83f3d1 | 639 | upgrade_set_timeout(60*5); // Set up timeout, may also abort execution. |
775f811a | 640 | quiz_update_grades($quiz, 0, false); |
641 | $pbar->update($i, $count, "Updating Quiz grades ($i/$count)."); | |
d6dd2108 | 642 | } |
643 | } | |
3f2efc20 | 644 | $rs->close(); |
d0ac6bc2 | 645 | } |
646 | ||
d6dd2108 | 647 | /** |
a153c9f2 | 648 | * Create or update the grade item for given quiz |
d6dd2108 | 649 | * |
a153c9f2 | 650 | * @category grade |
d6dd2108 | 651 | * @param object $quiz object with extra cmidnumber |
8cc86111 | 652 | * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook |
d6dd2108 | 653 | * @return int 0 if ok, error code otherwise |
654 | */ | |
25a03faa | 655 | function quiz_grade_item_update($quiz, $grades = null) { |
3b1d5cc4 | 656 | global $CFG, $OUTPUT; |
609c0d6b | 657 | require_once($CFG->dirroot . '/mod/quiz/locallib.php'); |
25302dee | 658 | require_once($CFG->libdir.'/gradelib.php'); |
d6dd2108 | 659 | |
9e83f3d1 | 660 | if (array_key_exists('cmidnumber', $quiz)) { // May not be always present. |
25302dee | 661 | $params = array('itemname' => $quiz->name, 'idnumber' => $quiz->cmidnumber); |
d6dd2108 | 662 | } else { |
25302dee | 663 | $params = array('itemname' => $quiz->name); |
d6dd2108 | 664 | } |
665 | ||
666 | if ($quiz->grade > 0) { | |
667 | $params['gradetype'] = GRADE_TYPE_VALUE; | |
668 | $params['grademax'] = $quiz->grade; | |
669 | $params['grademin'] = 0; | |
670 | ||
671 | } else { | |
672 | $params['gradetype'] = GRADE_TYPE_NONE; | |
673 | } | |
674 | ||
9e83f3d1 | 675 | // What this is trying to do: |
25a03faa TH |
676 | // 1. If the quiz is set to not show grades while the quiz is still open, |
677 | // and is set to show grades after the quiz is closed, then create the | |
678 | // grade_item with a show-after date that is the quiz close date. | |
679 | // 2. If the quiz is set to not show grades at either of those times, | |
680 | // create the grade_item as hidden. | |
681 | // 3. If the quiz is set to show grades, create the grade_item visible. | |
25302dee TH |
682 | $openreviewoptions = mod_quiz_display_options::make_from_quiz($quiz, |
683 | mod_quiz_display_options::LATER_WHILE_OPEN); | |
684 | $closedreviewoptions = mod_quiz_display_options::make_from_quiz($quiz, | |
685 | mod_quiz_display_options::AFTER_CLOSE); | |
6d03fd98 TH |
686 | if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX && |
687 | $closedreviewoptions->marks < question_display_options::MARK_AND_MAX) { | |
1223d24a | 688 | $params['hidden'] = 1; |
689 | ||
6d03fd98 TH |
690 | } else if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX && |
691 | $closedreviewoptions->marks >= question_display_options::MARK_AND_MAX) { | |
1223d24a | 692 | if ($quiz->timeclose) { |
693 | $params['hidden'] = $quiz->timeclose; | |
694 | } else { | |
695 | $params['hidden'] = 1; | |
696 | } | |
697 | ||
698 | } else { | |
9e83f3d1 | 699 | // Either |
1223d24a | 700 | // a) both open and closed enabled |
25a03faa | 701 | // b) open enabled, closed disabled - we can not "hide after", |
9e83f3d1 | 702 | // grades are kept visible even after closing. |
1223d24a | 703 | $params['hidden'] = 0; |
704 | } | |
705 | ||
0b5a80a1 | 706 | if ($grades === 'reset') { |
707 | $params['reset'] = true; | |
25a03faa | 708 | $grades = null; |
0b5a80a1 | 709 | } |
9cf4a18b | 710 | |
49460d84 | 711 | $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id); |
d45459b7 | 712 | if (!empty($gradebook_grades->items)) { |
713 | $grade_item = $gradebook_grades->items[0]; | |
714 | if ($grade_item->locked) { | |
9e83f3d1 | 715 | // NOTE: this is an extremely nasty hack! It is not a bug if this confirmation fails badly. --skodak. |
d45459b7 | 716 | $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT); |
717 | if (!$confirm_regrade) { | |
718 | $message = get_string('gradeitemislocked', 'grades'); | |
25a03faa TH |
719 | $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id . |
720 | '&mode=overview'; | |
d45459b7 | 721 | $regrade_link = qualified_me() . '&confirm_regrade=1'; |
3b1d5cc4 | 722 | echo $OUTPUT->box_start('generalbox', 'notice'); |
d45459b7 | 723 | echo '<p>'. $message .'</p>'; |
39e37019 | 724 | echo $OUTPUT->container_start('buttons'); |
5c2ed7e2 PS |
725 | echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades')); |
726 | echo $OUTPUT->single_button($back_link, get_string('cancel')); | |
39e37019 | 727 | echo $OUTPUT->container_end(); |
3b1d5cc4 | 728 | echo $OUTPUT->box_end(); |
9cf4a18b | 729 | |
d45459b7 | 730 | return GRADE_UPDATE_ITEM_LOCKED; |
731 | } | |
49460d84 | 732 | } |
733 | } | |
0b5a80a1 | 734 | |
ced5ee59 | 735 | return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params); |
d6dd2108 | 736 | } |
737 | ||
738 | /** | |
739 | * Delete grade item for given quiz | |
740 | * | |
a153c9f2 | 741 | * @category grade |
d6dd2108 | 742 | * @param object $quiz object |
743 | * @return object quiz | |
744 | */ | |
745 | function quiz_grade_item_delete($quiz) { | |
746 | global $CFG; | |
53004e48 | 747 | require_once($CFG->libdir . '/gradelib.php'); |
d6dd2108 | 748 | |
25a03faa TH |
749 | return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, |
750 | null, array('deleted' => 1)); | |
d6dd2108 | 751 | } |
752 | ||
8cc86111 | 753 | /** |
754 | * This standard function will check all instances of this module | |
755 | * and make sure there are up-to-date events created for each of them. | |
756 | * If courseid = 0, then every quiz event in the site is checked, else | |
757 | * only quiz events belonging to the course specified are checked. | |
758 | * This function is used, in its new format, by restore_refresh_events() | |
759 | * | |
8cc86111 | 760 | * @param int $courseid |
761 | * @return bool | |
762 | */ | |
d2f308c0 | 763 | function quiz_refresh_events($courseid = 0) { |
9cf4a18b | 764 | global $DB; |
d2f308c0 | 765 | |
766 | if ($courseid == 0) { | |
25302dee | 767 | if (!$quizzes = $DB->get_records('quiz')) { |
d2f308c0 | 768 | return true; |
769 | } | |
770 | } else { | |
25302dee | 771 | if (!$quizzes = $DB->get_records('quiz', array('course' => $courseid))) { |
d2f308c0 | 772 | return true; |
773 | } | |
774 | } | |
f41e824f | 775 | |
d2f308c0 | 776 | foreach ($quizzes as $quiz) { |
990650f9 | 777 | quiz_update_events($quiz); |
d2f308c0 | 778 | } |
990650f9 | 779 | |
d2f308c0 | 780 | return true; |
781 | } | |
782 | ||
dd97c328 | 783 | /** |
784 | * Returns all quiz graded users since a given time for specified quiz | |
785 | */ | |
8d297188 TH |
786 | function quiz_get_recent_mod_activity(&$activities, &$index, $timestart, |
787 | $courseid, $cmid, $userid = 0, $groupid = 0) { | |
9cf4a18b | 788 | global $CFG, $COURSE, $USER, $DB; |
8d297188 | 789 | require_once('locallib.php'); |
6710ec87 | 790 | |
dd97c328 | 791 | if ($COURSE->id == $courseid) { |
792 | $course = $COURSE; | |
6710ec87 | 793 | } else { |
9cf4a18b | 794 | $course = $DB->get_record('course', array('id' => $courseid)); |
6710ec87 | 795 | } |
6710ec87 | 796 | |
f20edd52 | 797 | $modinfo = get_fast_modinfo($course); |
6710ec87 | 798 | |
dd97c328 | 799 | $cm = $modinfo->cms[$cmid]; |
8d297188 | 800 | $quiz = $DB->get_record('quiz', array('id' => $cm->instance)); |
9cf4a18b | 801 | |
dd97c328 | 802 | if ($userid) { |
8d297188 TH |
803 | $userselect = "AND u.id = :userid"; |
804 | $params['userid'] = $userid; | |
dd97c328 | 805 | } else { |
8d297188 | 806 | $userselect = ''; |
dd97c328 | 807 | } |
ac21ad39 | 808 | |
dd97c328 | 809 | if ($groupid) { |
8d297188 TH |
810 | $groupselect = 'AND gm.groupid = :groupid'; |
811 | $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id'; | |
812 | $params['groupid'] = $groupid; | |
dd97c328 | 813 | } else { |
8d297188 TH |
814 | $groupselect = ''; |
815 | $groupjoin = ''; | |
816 | } | |
817 | ||
122fc5d9 DM |
818 | $params['timestart'] = $timestart; |
819 | $params['quizid'] = $quiz->id; | |
820 | ||
8d297188 TH |
821 | if (!$attempts = $DB->get_records_sql(" |
822 | SELECT qa.*, | |
823 | u.firstname, u.lastname, u.email, u.picture, u.imagealt | |
824 | FROM {quiz_attempts} qa | |
825 | JOIN {user} u ON u.id = qa.userid | |
826 | $groupjoin | |
827 | WHERE qa.timefinish > :timestart | |
828 | AND qa.quiz = :quizid | |
829 | AND qa.preview = 0 | |
830 | $userselect | |
831 | $groupselect | |
832 | ORDER BY qa.timefinish ASC", $params)) { | |
833 | return; | |
834 | } | |
835 | ||
836 | $context = get_context_instance(CONTEXT_MODULE, $cm->id); | |
8d297188 TH |
837 | $accessallgroups = has_capability('moodle/site:accessallgroups', $context); |
838 | $viewfullnames = has_capability('moodle/site:viewfullnames', $context); | |
cb323d02 | 839 | $grader = has_capability('mod/quiz:viewreports', $context); |
dd97c328 | 840 | $groupmode = groups_get_activity_groupmode($cm, $course); |
6710ec87 | 841 | |
dd97c328 | 842 | if (is_null($modinfo->groups)) { |
9e83f3d1 | 843 | // Load all my groups and cache it in modinfo. |
25a03faa | 844 | $modinfo->groups = groups_get_user_groups($course->id); |
dd97c328 | 845 | } |
6710ec87 | 846 | |
8d297188 | 847 | $usersgroups = null; |
25a03faa | 848 | $aname = format_string($cm->name, true); |
dd97c328 | 849 | foreach ($attempts as $attempt) { |
850 | if ($attempt->userid != $USER->id) { | |
851 | if (!$grader) { | |
9e83f3d1 | 852 | // Grade permission required. |
dd97c328 | 853 | continue; |
854 | } | |
6710ec87 | 855 | |
9cf4a18b | 856 | if ($groupmode == SEPARATEGROUPS and !$accessallgroups) { |
8d297188 TH |
857 | if (is_null($usersgroups)) { |
858 | $usersgroups = groups_get_all_groups($course->id, | |
859 | $attempt->userid, $cm->groupingid); | |
860 | if (is_array($usersgroups)) { | |
861 | $usersgroups = array_keys($usersgroups); | |
862 | } else { | |
863 | $usersgroups = array(); | |
864 | } | |
dd97c328 | 865 | } |
8d297188 | 866 | if (!array_intersect($usersgroups, $modinfo->groups[$cm->id])) { |
dd97c328 | 867 | continue; |
868 | } | |
869 | } | |
8d297188 TH |
870 | } |
871 | ||
7ee80cab | 872 | $options = quiz_get_review_options($quiz, $attempt, $context); |
dd97c328 | 873 | |
0ff4bd08 | 874 | $tmpactivity = new stdClass(); |
dd97c328 | 875 | |
8d297188 TH |
876 | $tmpactivity->type = 'quiz'; |
877 | $tmpactivity->cmid = $cm->id; | |
878 | $tmpactivity->name = $aname; | |
879 | $tmpactivity->sectionnum = $cm->sectionnum; | |
880 | $tmpactivity->timestamp = $attempt->timefinish; | |
9cf4a18b | 881 | |
dd97c328 | 882 | $tmpactivity->content->attemptid = $attempt->id; |
dd97c328 | 883 | $tmpactivity->content->attempt = $attempt->attempt; |
6d03fd98 | 884 | if (quiz_has_grades($quiz) && $options->marks >= question_display_options::MARK_AND_MAX) { |
8d297188 TH |
885 | $tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades); |
886 | $tmpactivity->content->maxgrade = quiz_format_grade($quiz, $quiz->sumgrades); | |
887 | } else { | |
888 | $tmpactivity->content->sumgrades = null; | |
889 | $tmpactivity->content->maxgrade = null; | |
890 | } | |
9cf4a18b | 891 | |
2a27a37d | 892 | $tmpactivity->user->id = $attempt->userid; |
8d297188 | 893 | $tmpactivity->user->firstname = $attempt->firstname; |
25302dee TH |
894 | $tmpactivity->user->lastname = $attempt->lastname; |
895 | $tmpactivity->user->fullname = fullname($attempt, $viewfullnames); | |
896 | $tmpactivity->user->picture = $attempt->picture; | |
897 | $tmpactivity->user->imagealt = $attempt->imagealt; | |
898 | $tmpactivity->user->email = $attempt->email; | |
9cf4a18b | 899 | |
dd97c328 | 900 | $activities[$index++] = $tmpactivity; |
6710ec87 | 901 | } |
6710ec87 | 902 | } |
903 | ||
dd97c328 | 904 | function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) { |
e63f88c9 | 905 | global $CFG, $OUTPUT; |
6710ec87 | 906 | |
dd97c328 | 907 | echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">'; |
6710ec87 | 908 | |
8d297188 TH |
909 | echo '<tr><td class="userpicture" valign="top">'; |
910 | echo $OUTPUT->user_picture($activity->user, array('courseid' => $courseid)); | |
911 | echo '</td><td>'; | |
6710ec87 | 912 | |
913 | if ($detail) { | |
dd97c328 | 914 | $modname = $modnames[$activity->type]; |
915 | echo '<div class="title">'; | |
8d297188 TH |
916 | echo '<img src="' . $OUTPUT->pix_url('icon', $activity->type) . '" ' . |
917 | 'class="icon" alt="' . $modname . '" />'; | |
918 | echo '<a href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' . | |
919 | $activity->cmid . '">' . $activity->name . '</a>'; | |
dd97c328 | 920 | echo '</div>'; |
6710ec87 | 921 | } |
922 | ||
dd97c328 | 923 | echo '<div class="grade">'; |
8d297188 TH |
924 | echo get_string('attempt', 'quiz', $activity->content->attempt); |
925 | if (isset($activity->content->maxgrade)) { | |
926 | $grades = $activity->content->sumgrades . ' / ' . $activity->content->maxgrade; | |
927 | echo ': (<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . | |
928 | $activity->content->attemptid . '">' . $grades . '</a>)'; | |
929 | } | |
dd97c328 | 930 | echo '</div>'; |
6710ec87 | 931 | |
dd97c328 | 932 | echo '<div class="user">'; |
8d297188 TH |
933 | echo '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $activity->user->id . |
934 | '&course=' . $courseid . '">' . $activity->user->fullname . | |
935 | '</a> - ' . userdate($activity->timestamp); | |
dd97c328 | 936 | echo '</div>'; |
6710ec87 | 937 | |
8d297188 | 938 | echo '</td></tr></table>'; |
6710ec87 | 939 | |
940 | return; | |
941 | } | |
942 | ||
ee1fb969 | 943 | /** |
920b93d1 | 944 | * Pre-process the quiz options form data, making any necessary adjustments. |
ad4cd837 | 945 | * Called by add/update instance in this file. |
b159da78 | 946 | * |
920b93d1 | 947 | * @param object $quiz The variables set on the form. |
948 | */ | |
25302dee TH |
949 | function quiz_process_options($quiz) { |
950 | global $CFG; | |
951 | require_once($CFG->dirroot . '/mod/quiz/locallib.php'); | |
952 | require_once($CFG->libdir . '/questionlib.php'); | |
953 | ||
920b93d1 | 954 | $quiz->timemodified = time(); |
ee1fb969 | 955 | |
dc5c6851 | 956 | // Quiz name. |
957 | if (!empty($quiz->name)) { | |
958 | $quiz->name = trim($quiz->name); | |
959 | } | |
a23f0aaf | 960 | |
ab0a8ff2 | 961 | // Password field - different in form to stop browsers that remember passwords |
962 | // getting confused. | |
963 | $quiz->password = $quiz->quizpassword; | |
964 | unset($quiz->quizpassword); | |
965 | ||
9e83f3d1 | 966 | // Quiz feedback. |
a0807a00 | 967 | if (isset($quiz->feedbacktext)) { |
968 | // Clean up the boundary text. | |
969 | for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) { | |
fe6ce234 DC |
970 | if (empty($quiz->feedbacktext[$i]['text'])) { |
971 | $quiz->feedbacktext[$i]['text'] = ''; | |
a0807a00 | 972 | } else { |
fe6ce234 | 973 | $quiz->feedbacktext[$i]['text'] = trim($quiz->feedbacktext[$i]['text']); |
a0807a00 | 974 | } |
212b7b8c | 975 | } |
b159da78 | 976 | |
a0807a00 | 977 | // Check the boundary value is a number or a percentage, and in range. |
978 | $i = 0; | |
979 | while (!empty($quiz->feedbackboundaries[$i])) { | |
980 | $boundary = trim($quiz->feedbackboundaries[$i]); | |
981 | if (!is_numeric($boundary)) { | |
982 | if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') { | |
983 | $boundary = trim(substr($boundary, 0, -1)); | |
984 | if (is_numeric($boundary)) { | |
985 | $boundary = $boundary * $quiz->grade / 100.0; | |
986 | } else { | |
987 | return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1); | |
988 | } | |
212b7b8c | 989 | } |
990 | } | |
a0807a00 | 991 | if ($boundary <= 0 || $boundary >= $quiz->grade) { |
992 | return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1); | |
993 | } | |
994 | if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) { | |
995 | return get_string('feedbackerrororder', 'quiz', $i + 1); | |
996 | } | |
997 | $quiz->feedbackboundaries[$i] = $boundary; | |
998 | $i += 1; | |
212b7b8c | 999 | } |
a0807a00 | 1000 | $numboundaries = $i; |
b159da78 | 1001 | |
a0807a00 | 1002 | // Check there is nothing in the remaining unused fields. |
e0b7cfcb | 1003 | if (!empty($quiz->feedbackboundaries)) { |
1004 | for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) { | |
25a03faa TH |
1005 | if (!empty($quiz->feedbackboundaries[$i]) && |
1006 | trim($quiz->feedbackboundaries[$i]) != '') { | |
e0b7cfcb | 1007 | return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1); |
1008 | } | |
a0807a00 | 1009 | } |
212b7b8c | 1010 | } |
a0807a00 | 1011 | for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) { |
25a03faa TH |
1012 | if (!empty($quiz->feedbacktext[$i]['text']) && |
1013 | trim($quiz->feedbacktext[$i]['text']) != '') { | |
a0807a00 | 1014 | return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1); |
1015 | } | |
212b7b8c | 1016 | } |
25a03faa TH |
1017 | // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade(). |
1018 | $quiz->feedbackboundaries[-1] = $quiz->grade + 1; | |
a0807a00 | 1019 | $quiz->feedbackboundaries[$numboundaries] = 0; |
1020 | $quiz->feedbackboundarycount = $numboundaries; | |
212b7b8c | 1021 | } |
a23f0aaf | 1022 | |
25302dee TH |
1023 | // Combing the individual settings into the review columns. |
1024 | $quiz->reviewattempt = quiz_review_option_form_to_db($quiz, 'attempt'); | |
1025 | $quiz->reviewcorrectness = quiz_review_option_form_to_db($quiz, 'correctness'); | |
1026 | $quiz->reviewmarks = quiz_review_option_form_to_db($quiz, 'marks'); | |
1027 | $quiz->reviewspecificfeedback = quiz_review_option_form_to_db($quiz, 'specificfeedback'); | |
1028 | $quiz->reviewgeneralfeedback = quiz_review_option_form_to_db($quiz, 'generalfeedback'); | |
1029 | $quiz->reviewrightanswer = quiz_review_option_form_to_db($quiz, 'rightanswer'); | |
1030 | $quiz->reviewoverallfeedback = quiz_review_option_form_to_db($quiz, 'overallfeedback'); | |
1031 | $quiz->reviewattempt |= mod_quiz_display_options::DURING; | |
1032 | $quiz->reviewoverallfeedback &= ~mod_quiz_display_options::DURING; | |
1033 | } | |
ee1fb969 | 1034 | |
25302dee TH |
1035 | /** |
1036 | * Helper function for {@link quiz_process_options()}. | |
1037 | * @param object $fromform the sumbitted form date. | |
1038 | * @param string $field one of the review option field names. | |
1039 | */ | |
1040 | function quiz_review_option_form_to_db($fromform, $field) { | |
1041 | static $times = array( | |
1042 | 'during' => mod_quiz_display_options::DURING, | |
1043 | 'immediately' => mod_quiz_display_options::IMMEDIATELY_AFTER, | |
1044 | 'open' => mod_quiz_display_options::LATER_WHILE_OPEN, | |
1045 | 'closed' => mod_quiz_display_options::AFTER_CLOSE, | |
1046 | ); | |
00719c02 | 1047 | |
25302dee TH |
1048 | $review = 0; |
1049 | foreach ($times as $whenname => $when) { | |
1050 | $fieldname = $field . $whenname; | |
1051 | if (isset($fromform->$fieldname)) { | |
1052 | $review |= $when; | |
1053 | unset($fromform->$fieldname); | |
1054 | } | |
1b8a7434 | 1055 | } |
1056 | ||
25302dee | 1057 | return $review; |
920b93d1 | 1058 | } |
1059 | ||
1060 | /** | |
1061 | * This function is called at the end of quiz_add_instance | |
1062 | * and quiz_update_instance, to do the common processing. | |
a23f0aaf | 1063 | * |
920b93d1 | 1064 | * @param object $quiz the quiz object. |
1065 | */ | |
1066 | function quiz_after_add_or_update($quiz) { | |
c18269c7 | 1067 | global $DB; |
fe6ce234 DC |
1068 | $cmid = $quiz->coursemodule; |
1069 | ||
9e83f3d1 | 1070 | // We need to use context now, so we need to make sure all needed info is already in db. |
fe6ce234 DC |
1071 | $DB->set_field('course_modules', 'instance', $quiz->id, array('id'=>$cmid)); |
1072 | $context = get_context_instance(CONTEXT_MODULE, $cmid); | |
920b93d1 | 1073 | |
9e83f3d1 | 1074 | // Save the feedback. |
53004e48 | 1075 | $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id)); |
a23f0aaf | 1076 | |
fe6ce234 | 1077 | for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) { |
0ff4bd08 | 1078 | $feedback = new stdClass(); |
212b7b8c | 1079 | $feedback->quizid = $quiz->id; |
fe6ce234 DC |
1080 | $feedback->feedbacktext = $quiz->feedbacktext[$i]['text']; |
1081 | $feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format']; | |
212b7b8c | 1082 | $feedback->mingrade = $quiz->feedbackboundaries[$i]; |
1083 | $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1]; | |
fe6ce234 | 1084 | $feedback->id = $DB->insert_record('quiz_feedback', $feedback); |
25a03faa TH |
1085 | $feedbacktext = file_save_draft_area_files((int)$quiz->feedbacktext[$i]['itemid'], |
1086 | $context->id, 'mod_quiz', 'feedback', $feedback->id, | |
1087 | array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0), | |
1088 | $quiz->feedbacktext[$i]['text']); | |
1089 | $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext, | |
1090 | array('id' => $feedback->id)); | |
212b7b8c | 1091 | } |
1092 | ||
b83c32d3 TH |
1093 | // Store any settings belonging to the access rules. |
1094 | quiz_access_manager::save_settings($quiz); | |
1095 | ||
920b93d1 | 1096 | // Update the events relating to this quiz. |
990650f9 TH |
1097 | quiz_update_events($quiz); |
1098 | ||
9e83f3d1 | 1099 | // Update related grade item. |
990650f9 | 1100 | quiz_grade_item_update($quiz); |
990650f9 TH |
1101 | } |
1102 | ||
1103 | /** | |
1104 | * This function updates the events associated to the quiz. | |
1105 | * If $override is non-zero, then it updates only the events | |
1106 | * associated with the specified override. | |
1107 | * | |
1108 | * @uses QUIZ_MAX_EVENT_LENGTH | |
1109 | * @param object $quiz the quiz object. | |
1110 | * @param object optional $override limit to a specific override | |
1111 | */ | |
1112 | function quiz_update_events($quiz, $override = null) { | |
1113 | global $DB; | |
1114 | ||
1115 | // Load the old events relating to this quiz. | |
1116 | $conds = array('modulename'=>'quiz', | |
1117 | 'instance'=>$quiz->id); | |
1118 | if (!empty($override)) { | |
9e83f3d1 | 1119 | // Only load events for this override. |
990650f9 TH |
1120 | $conds['groupid'] = isset($override->groupid)? $override->groupid : 0; |
1121 | $conds['userid'] = isset($override->userid)? $override->userid : 0; | |
1122 | } | |
1123 | $oldevents = $DB->get_records('event', $conds); | |
1124 | ||
9e83f3d1 | 1125 | // Now make a todo list of all that needs to be updated. |
990650f9 TH |
1126 | if (empty($override)) { |
1127 | // We are updating the primary settings for the quiz, so we | |
9e83f3d1 | 1128 | // need to add all the overrides. |
990650f9 | 1129 | $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id)); |
9e83f3d1 | 1130 | // As well as the original quiz (empty override). |
0ff4bd08 | 1131 | $overrides[] = new stdClass(); |
25a03faa | 1132 | } else { |
9e83f3d1 | 1133 | // Just do the one override. |
990650f9 | 1134 | $overrides = array($override); |
920b93d1 | 1135 | } |
1136 | ||
990650f9 TH |
1137 | foreach ($overrides as $current) { |
1138 | $groupid = isset($current->groupid)? $current->groupid : 0; | |
1139 | $userid = isset($current->userid)? $current->userid : 0; | |
1140 | $timeopen = isset($current->timeopen)? $current->timeopen : $quiz->timeopen; | |
1141 | $timeclose = isset($current->timeclose)? $current->timeclose : $quiz->timeclose; | |
1142 | ||
9e83f3d1 | 1143 | // Only add open/close events for an override if they differ from the quiz default. |
990650f9 TH |
1144 | $addopen = empty($current->id) || !empty($current->timeopen); |
1145 | $addclose = empty($current->id) || !empty($current->timeclose); | |
1146 | ||
0ff4bd08 | 1147 | $event = new stdClass(); |
8180ee23 | 1148 | $event->description = format_module_intro('quiz', $quiz, $quiz->coursemodule); |
9e83f3d1 | 1149 | // Events module won't show user events when the courseid is nonzero. |
25a03faa | 1150 | $event->courseid = ($userid) ? 0 : $quiz->course; |
990650f9 TH |
1151 | $event->groupid = $groupid; |
1152 | $event->userid = $userid; | |
1153 | $event->modulename = 'quiz'; | |
1154 | $event->instance = $quiz->id; | |
1155 | $event->timestart = $timeopen; | |
1156 | $event->timeduration = max($timeclose - $timeopen, 0); | |
1157 | $event->visible = instance_is_visible('quiz', $quiz); | |
1158 | $event->eventtype = 'open'; | |
1159 | ||
9e83f3d1 | 1160 | // Determine the event name. |
990650f9 | 1161 | if ($groupid) { |
0ff4bd08 | 1162 | $params = new stdClass(); |
990650f9 TH |
1163 | $params->quiz = $quiz->name; |
1164 | $params->group = groups_get_group_name($groupid); | |
1165 | if ($params->group === false) { | |
9e83f3d1 | 1166 | // Group doesn't exist, just skip it. |
990650f9 TH |
1167 | continue; |
1168 | } | |
1169 | $eventname = get_string('overridegroupeventname', 'quiz', $params); | |
25a03faa | 1170 | } else if ($userid) { |
0ff4bd08 | 1171 | $params = new stdClass(); |
990650f9 TH |
1172 | $params->quiz = $quiz->name; |
1173 | $eventname = get_string('overrideusereventname', 'quiz', $params); | |
1174 | } else { | |
1175 | $eventname = $quiz->name; | |
1176 | } | |
1177 | if ($addopen or $addclose) { | |
1178 | if ($timeclose and $timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) { | |
1179 | // Single event for the whole quiz. | |
1180 | if ($oldevent = array_shift($oldevents)) { | |
1181 | $event->id = $oldevent->id; | |
25a03faa | 1182 | } else { |
990650f9 TH |
1183 | unset($event->id); |
1184 | } | |
1185 | $event->name = $eventname; | |
9e83f3d1 | 1186 | // The method calendar_event::create will reuse a db record if the id field is set. |
990650f9 TH |
1187 | calendar_event::create($event); |
1188 | } else { | |
1189 | // Separate start and end events. | |
1190 | $event->timeduration = 0; | |
1191 | if ($timeopen && $addopen) { | |
1192 | if ($oldevent = array_shift($oldevents)) { | |
1193 | $event->id = $oldevent->id; | |
25a03faa | 1194 | } else { |
990650f9 TH |
1195 | unset($event->id); |
1196 | } | |
1197 | $event->name = $eventname.' ('.get_string('quizopens', 'quiz').')'; | |
9e83f3d1 | 1198 | // The method calendar_event::create will reuse a db record if the id field is set. |
990650f9 TH |
1199 | calendar_event::create($event); |
1200 | } | |
1201 | if ($timeclose && $addclose) { | |
1202 | if ($oldevent = array_shift($oldevents)) { | |
1203 | $event->id = $oldevent->id; | |
25a03faa | 1204 | } else { |
990650f9 TH |
1205 | unset($event->id); |
1206 | } | |
1207 | $event->name = $eventname.' ('.get_string('quizcloses', 'quiz').')'; | |
1208 | $event->timestart = $timeclose; | |
1209 | $event->eventtype = 'close'; | |
1210 | calendar_event::create($event); | |
1211 | } | |
1212 | } | |
920b93d1 | 1213 | } |
1214 | } | |
d6dd2108 | 1215 | |
9e83f3d1 | 1216 | // Delete any leftover events. |
990650f9 TH |
1217 | foreach ($oldevents as $badevent) { |
1218 | $badevent = calendar_event::load($badevent); | |
1219 | $badevent->delete(); | |
1220 | } | |
ee1fb969 | 1221 | } |
1222 | ||
8cc86111 | 1223 | /** |
1224 | * @return array | |
1225 | */ | |
f3221af9 | 1226 | function quiz_get_view_actions() { |
acf149ad | 1227 | return array('view', 'view all', 'report', 'review'); |
f3221af9 | 1228 | } |
ee1fb969 | 1229 | |
8cc86111 | 1230 | /** |
1231 | * @return array | |
1232 | */ | |
f3221af9 | 1233 | function quiz_get_post_actions() { |
25a03faa TH |
1234 | return array('attempt', 'close attempt', 'preview', 'editquestions', |
1235 | 'delete attempt', 'manualgrade'); | |
f3221af9 | 1236 | } |
ee1fb969 | 1237 | |
f67172b6 | 1238 | /** |
25302dee | 1239 | * @param array $questionids of question ids. |
f7970e3c | 1240 | * @return bool whether any of these questions are used by any instance of this module. |
f67172b6 | 1241 | */ |
25302dee | 1242 | function quiz_questions_in_use($questionids) { |
07f88584 TH |
1243 | global $DB, $CFG; |
1244 | require_once($CFG->libdir . '/questionlib.php'); | |
25302dee TH |
1245 | list($test, $params) = $DB->get_in_or_equal($questionids); |
1246 | return $DB->record_exists_select('quiz_question_instances', | |
07f88584 | 1247 | 'question ' . $test, $params) || question_engine::questions_in_use( |
6b5f24d3 | 1248 | $questionids, new qubaid_join('{quiz_attempts} quiza', |
07f88584 | 1249 | 'quiza.uniqueid', 'quiza.preview = 0')); |
f67172b6 | 1250 | } |
1251 | ||
7a6f4066 | 1252 | /** |
1253 | * Implementation of the function for printing the form elements that control | |
1254 | * whether the course reset functionality affects the quiz. | |
3b1d5cc4 | 1255 | * |
25302dee | 1256 | * @param $mform the course reset form that is being built. |
0b5a80a1 | 1257 | */ |
25302dee | 1258 | function quiz_reset_course_form_definition($mform) { |
c159da4c | 1259 | $mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz')); |
25a03faa TH |
1260 | $mform->addElement('advcheckbox', 'reset_quiz_attempts', |
1261 | get_string('removeallquizattempts', 'quiz')); | |
0b5a80a1 | 1262 | } |
1263 | ||
1264 | /** | |
1265 | * Course reset form defaults. | |
25302dee | 1266 | * @return array the defaults. |
0b5a80a1 | 1267 | */ |
1268 | function quiz_reset_course_form_defaults($course) { | |
25302dee | 1269 | return array('reset_quiz_attempts' => 1); |
0b5a80a1 | 1270 | } |
1271 | ||
1272 | /** | |
1273 | * Removes all grades from gradebook | |
8cc86111 | 1274 | * |
0b5a80a1 | 1275 | * @param int $courseid |
1276 | * @param string optional type | |
7a6f4066 | 1277 | */ |
0b5a80a1 | 1278 | function quiz_reset_gradebook($courseid, $type='') { |
9cf4a18b | 1279 | global $CFG, $DB; |
0b5a80a1 | 1280 | |
25302dee TH |
1281 | $quizzes = $DB->get_records_sql(" |
1282 | SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid | |
1283 | FROM {modules} m | |
1284 | JOIN {course_modules} cm ON m.id = cm.module | |
1285 | JOIN {quiz} q ON cm.instance = q.id | |
1286 | WHERE m.name = 'quiz' AND cm.course = ?", array($courseid)); | |
0b5a80a1 | 1287 | |
25302dee TH |
1288 | foreach ($quizzes as $quiz) { |
1289 | quiz_grade_item_update($quiz, 'reset'); | |
0b5a80a1 | 1290 | } |
7a6f4066 | 1291 | } |
1292 | ||
1293 | /** | |
72d2982e | 1294 | * Actual implementation of the reset course functionality, delete all the |
7a6f4066 | 1295 | * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is |
1296 | * set and true. | |
6ef56c99 | 1297 | * |
1298 | * Also, move the quiz open and close dates, if the course start date is changing. | |
8cc86111 | 1299 | * |
8cc86111 | 1300 | * @param object $data the data submitted from the reset course. |
0b5a80a1 | 1301 | * @return array status array |
7a6f4066 | 1302 | */ |
0b5a80a1 | 1303 | function quiz_reset_userdata($data) { |
53004e48 | 1304 | global $CFG, $DB; |
1305 | require_once($CFG->libdir.'/questionlib.php'); | |
be0ba083 | 1306 | |
0b5a80a1 | 1307 | $componentstr = get_string('modulenameplural', 'quiz'); |
1308 | $status = array(); | |
b159da78 | 1309 | |
25a03faa | 1310 | // Delete attempts. |
6ef56c99 | 1311 | if (!empty($data->reset_quiz_attempts)) { |
6b5f24d3 TH |
1312 | require_once($CFG->libdir . '/questionlib.php'); |
1313 | ||
1314 | question_engine::delete_questions_usage_by_activities(new qubaid_join( | |
1315 | '{quiz_attempts} quiza JOIN {quiz} quiz ON quiza.quiz = quiz.id', | |
1316 | 'quiza.uniqueid', 'quiz.course = :quizcourseid', | |
1317 | array('quizcourseid' => $data->courseid))); | |
25302dee TH |
1318 | |
1319 | $DB->delete_records_select('quiz_attempts', | |
1320 | 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid)); | |
1321 | $status[] = array( | |
1322 | 'component' => $componentstr, | |
25a03faa | 1323 | 'item' => get_string('attemptsdeleted', 'quiz'), |
25302dee TH |
1324 | 'error' => false); |
1325 | ||
9e83f3d1 | 1326 | // Remove all grades from gradebook. |
8dd9ccf4 TH |
1327 | $DB->delete_records_select('quiz_grades', |
1328 | 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid)); | |
0b5a80a1 | 1329 | if (empty($data->reset_gradebook_grades)) { |
1330 | quiz_reset_gradebook($data->courseid); | |
7a6f4066 | 1331 | } |
25302dee TH |
1332 | $status[] = array( |
1333 | 'component' => $componentstr, | |
8dd9ccf4 | 1334 | 'item' => get_string('gradesdeleted', 'quiz'), |
25302dee | 1335 | 'error' => false); |
7a6f4066 | 1336 | } |
6ef56c99 | 1337 | |
9e83f3d1 | 1338 | // Updating dates - shift may be negative too. |
0b5a80a1 | 1339 | if ($data->timeshift) { |
25a03faa TH |
1340 | shift_course_mod_dates('quiz', array('timeopen', 'timeclose'), |
1341 | $data->timeshift, $data->courseid); | |
25302dee TH |
1342 | $status[] = array( |
1343 | 'component' => $componentstr, | |
1344 | 'item' => get_string('openclosedatesupdated', 'quiz'), | |
1345 | 'error' => false); | |
7a6f4066 | 1346 | } |
0b5a80a1 | 1347 | |
1348 | return $status; | |
7a6f4066 | 1349 | } |
14e6dc79 | 1350 | |
1351 | /** | |
1352 | * Checks whether the current user is allowed to view a file uploaded in a quiz. | |
1353 | * Teachers can view any from their courses, students can only view their own. | |
b159da78 | 1354 | * |
95de57b8 | 1355 | * @param int $attemptuniqueid int attempt id |
14e6dc79 | 1356 | * @param int $questionid int question id |
f7970e3c | 1357 | * @return bool to indicate access granted or denied |
14e6dc79 | 1358 | */ |
fe6ce234 DC |
1359 | function quiz_check_file_access($attemptuniqueid, $questionid, $context = null) { |
1360 | global $USER, $DB, $CFG; | |
1361 | require_once(dirname(__FILE__).'/attemptlib.php'); | |
1362 | require_once(dirname(__FILE__).'/locallib.php'); | |
b159da78 | 1363 | |
6102a59d | 1364 | $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $attemptuniqueid)); |
fe6ce234 DC |
1365 | $attemptobj = quiz_attempt::create($attempt->id); |
1366 | ||
9e83f3d1 | 1367 | // Does the question exist? |
fe6ce234 DC |
1368 | if (!$question = $DB->get_record('question', array('id' => $questionid))) { |
1369 | return false; | |
1370 | } | |
1371 | ||
1372 | if ($context === null) { | |
1373 | $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz)); | |
1374 | $cm = get_coursemodule_from_id('quiz', $quiz->id); | |
1375 | $context = get_context_instance(CONTEXT_MODULE, $cm->id); | |
1376 | } | |
1377 | ||
1378 | // Load those questions and the associated states. | |
1379 | $attemptobj->load_questions(array($questionid)); | |
1380 | $attemptobj->load_question_states(array($questionid)); | |
1381 | ||
9e83f3d1 | 1382 | // Obtain the state. |
fe6ce234 | 1383 | $state = $attemptobj->get_question_state($questionid); |
9e83f3d1 | 1384 | // Obtain the question. |
fe6ce234 | 1385 | $question = $attemptobj->get_question($questionid); |
b159da78 | 1386 | |
9e83f3d1 | 1387 | // Access granted if the current user submitted this file. |
fe6ce234 DC |
1388 | if ($attempt->userid != $USER->id) { |
1389 | return false; | |
fe6ce234 | 1390 | } |
9e83f3d1 | 1391 | // Access granted if the current user has permission to grade quizzes in this course. |
25a03faa TH |
1392 | if (!(has_capability('mod/quiz:viewreports', $context) || |
1393 | has_capability('mod/quiz:grade', $context))) { | |
fe6ce234 | 1394 | return false; |
14e6dc79 | 1395 | } |
b159da78 | 1396 | |
fe6ce234 | 1397 | return array($question, $state, array()); |
14e6dc79 | 1398 | } |
b5a16eb7 | 1399 | |
1400 | /** | |
1401 | * Prints quiz summaries on MyMoodle Page | |
8cc86111 | 1402 | * @param arry $courses |
1403 | * @param array $htmlarray | |
b5a16eb7 | 1404 | */ |
1405 | function quiz_print_overview($courses, &$htmlarray) { | |
1406 | global $USER, $CFG; | |
9e83f3d1 | 1407 | // These next 6 Lines are constant in all modules (just change module name). |
b5a16eb7 | 1408 | if (empty($courses) || !is_array($courses) || count($courses) == 0) { |
1409 | return array(); | |
1410 | } | |
1411 | ||
2a13e454 | 1412 | if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) { |
b5a16eb7 | 1413 | return; |
1414 | } | |
1415 | ||
25a03faa | 1416 | // Fetch some language strings outside the main loop. |
b5a16eb7 | 1417 | $strquiz = get_string('modulename', 'quiz'); |
1418 | $strnoattempts = get_string('noattempts', 'quiz'); | |
1419 | ||
25a03faa TH |
1420 | // We want to list quizzes that are currently available, and which have a close date. |
1421 | // This is the same as what the lesson does, and the dabate is in MDL-10568. | |
6c58e198 | 1422 | $now = time(); |
2a13e454 | 1423 | foreach ($quizzes as $quiz) { |
b5a16eb7 | 1424 | if ($quiz->timeclose >= $now && $quiz->timeopen < $now) { |
25a03faa | 1425 | // Give a link to the quiz, and the deadline. |
b5a16eb7 | 1426 | $str = '<div class="quiz overview">' . |
25a03faa TH |
1427 | '<div class="name">' . $strquiz . ': <a ' . |
1428 | ($quiz->visible ? '' : ' class="dimmed"') . | |
1429 | ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' . | |
1430 | $quiz->coursemodule . '">' . | |
b5a16eb7 | 1431 | $quiz->name . '</a></div>'; |
25a03faa TH |
1432 | $str .= '<div class="info">' . get_string('quizcloseson', 'quiz', |
1433 | userdate($quiz->timeclose)) . '</div>'; | |
b5a16eb7 | 1434 | |
25a03faa | 1435 | // Now provide more information depending on the uers's role. |
b5a16eb7 | 1436 | $context = get_context_instance(CONTEXT_MODULE, $quiz->coursemodule); |
1437 | if (has_capability('mod/quiz:viewreports', $context)) { | |
25a03faa | 1438 | // For teacher-like people, show a summary of the number of student attempts. |
9cf4a18b | 1439 | // The $quiz objects returned by get_all_instances_in_course have the necessary $cm |
2a13e454 | 1440 | // fields set to make the following call work. |
25a03faa TH |
1441 | $str .= '<div class="info">' . |
1442 | quiz_num_attempt_summary($quiz, $quiz, true) . '</div>'; | |
1443 | } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), | |
1444 | $context)) { // Student | |
1445 | // For student-like people, tell them how many attempts they have made. | |
1446 | if (isset($USER->id) && | |
1447 | ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) { | |
b5a16eb7 | 1448 | $numattempts = count($attempts); |
25a03faa TH |
1449 | $str .= '<div class="info">' . |
1450 | get_string('numattemptsmade', 'quiz', $numattempts) . '</div>'; | |
b5a16eb7 | 1451 | } else { |
1452 | $str .= '<div class="info">' . $strnoattempts . '</div>'; | |
1453 | } | |
1454 | } else { | |
25a03faa | 1455 | // For ayone else, there is no point listing this quiz, so stop processing. |
b5a16eb7 | 1456 | continue; |
1457 | } | |
1458 | ||
25a03faa | 1459 | // Add the output for this quiz to the rest. |
b5a16eb7 | 1460 | $str .= '</div>'; |
1461 | if (empty($htmlarray[$quiz->course]['quiz'])) { | |
1462 | $htmlarray[$quiz->course]['quiz'] = $str; | |
1463 | } else { | |
1464 | $htmlarray[$quiz->course]['quiz'] .= $str; | |
1465 | } | |
1466 | } | |
1467 | } | |
1468 | } | |
6c58e198 | 1469 | |
1470 | /** | |
25302dee | 1471 | * Return a textual summary of the number of attempts that have been made at a particular quiz, |
cd300cf3 | 1472 | * returns '' if no attempts have been made yet, unless $returnzero is passed as true. |
8cc86111 | 1473 | * |
6c58e198 | 1474 | * @param object $quiz the quiz object. Only $quiz->id is used at the moment. |
25a03faa TH |
1475 | * @param object $cm the cm object. Only $cm->course, $cm->groupmode and |
1476 | * $cm->groupingid fields are used at the moment. | |
1477 | * @param bool $returnzero if false (default), when no attempts have been | |
1478 | * made '' is returned instead of 'Attempts: 0'. | |
2a13e454 | 1479 | * @param int $currentgroup if there is a concept of current group where this method is being called |
1480 | * (e.g. a report) pass it in here. Default 0 which means no current group. | |
1481 | * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or | |
1482 | * "Attemtps 123 (45 from this group)". | |
6c58e198 | 1483 | */ |
2a13e454 | 1484 | function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) { |
a49cb927 | 1485 | global $DB, $USER; |
9cf4a18b | 1486 | $numattempts = $DB->count_records('quiz_attempts', array('quiz'=> $quiz->id, 'preview'=>0)); |
6c58e198 | 1487 | if ($numattempts || $returnzero) { |
2a13e454 | 1488 | if (groups_get_activity_groupmode($cm)) { |
92701024 | 1489 | $a = new stdClass(); |
2a13e454 | 1490 | $a->total = $numattempts; |
1491 | if ($currentgroup) { | |
3f11d7f9 | 1492 | $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' . |
9cf4a18b | 1493 | '{quiz_attempts} qa JOIN ' . |
1494 | '{groups_members} gm ON qa.userid = gm.userid ' . | |
25a03faa TH |
1495 | 'WHERE quiz = ? AND preview = 0 AND groupid = ?', |
1496 | array($quiz->id, $currentgroup)); | |
2a13e454 | 1497 | return get_string('attemptsnumthisgroup', 'quiz', $a); |
9cf4a18b | 1498 | } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) { |
1499 | list($usql, $params) = $DB->get_in_or_equal(array_keys($groups)); | |
3f11d7f9 | 1500 | $a->group = $DB->count_records_sql('SELECT COUNT(DISTINCT qa.id) FROM ' . |
9cf4a18b | 1501 | '{quiz_attempts} qa JOIN ' . |
1502 | '{groups_members} gm ON qa.userid = gm.userid ' . | |
1503 | 'WHERE quiz = ? AND preview = 0 AND ' . | |
1504 | "groupid $usql", array_merge(array($quiz->id), $params)); | |
2a13e454 | 1505 | return get_string('attemptsnumyourgroups', 'quiz', $a); |
1506 | } | |
1507 | } | |
6c58e198 | 1508 | return get_string('attemptsnum', 'quiz', $numattempts); |
1509 | } | |
1510 | return ''; | |
1511 | } | |
f432bebf | 1512 | |
4e781c7b | 1513 | /** |
a49cb927 TH |
1514 | * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link |
1515 | * to the quiz reports. | |
1516 | * | |
1517 | * @param object $quiz the quiz object. Only $quiz->id is used at the moment. | |
25a03faa TH |
1518 | * @param object $cm the cm object. Only $cm->course, $cm->groupmode and |
1519 | * $cm->groupingid fields are used at the moment. | |
a49cb927 | 1520 | * @param object $context the quiz context. |
25a03faa TH |
1521 | * @param bool $returnzero if false (default), when no attempts have been made |
1522 | * '' is returned instead of 'Attempts: 0'. | |
a49cb927 TH |
1523 | * @param int $currentgroup if there is a concept of current group where this method is being called |
1524 | * (e.g. a report) pass it in here. Default 0 which means no current group. | |
1525 | * @return string HTML fragment for the link. | |
1526 | */ | |
25a03faa TH |
1527 | function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false, |
1528 | $currentgroup = 0) { | |
a49cb927 TH |
1529 | global $CFG; |
1530 | $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup); | |
1531 | if (!$summary) { | |
1532 | return ''; | |
1533 | } | |
1534 | ||
1535 | require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); | |
1536 | $url = new moodle_url('/mod/quiz/report.php', array( | |
1537 | 'id' => $cm->id, 'mode' => quiz_report_default_report($context))); | |
1538 | return html_writer::link($url, $summary); | |
1539 | } | |
1540 | ||
1541 | /** | |
4e781c7b | 1542 | * @param string $feature FEATURE_xx constant for requested feature |
1543 | * @return bool True if quiz supports feature | |
1544 | */ | |
1545 | function quiz_supports($feature) { | |
1546 | switch($feature) { | |
42f103be | 1547 | case FEATURE_GROUPS: return true; |
1548 | case FEATURE_GROUPINGS: return true; | |
1549 | case FEATURE_GROUPMEMBERSONLY: return true; | |
dc5c2bd9 | 1550 | case FEATURE_MOD_INTRO: return true; |
4e781c7b | 1551 | case FEATURE_COMPLETION_TRACKS_VIEWS: return true; |
42f103be | 1552 | case FEATURE_GRADE_HAS_GRADE: return true; |
71c4154a | 1553 | case FEATURE_GRADE_OUTCOMES: return false; |
767cb7f0 | 1554 | case FEATURE_BACKUP_MOODLE2: return true; |
3e4c2435 | 1555 | case FEATURE_SHOW_DESCRIPTION: return true; |
42f103be | 1556 | |
49f6e5f4 | 1557 | default: return null; |
4e781c7b | 1558 | } |
1559 | } | |
1560 | ||
f432bebf | 1561 | /** |
cca6e300 | 1562 | * @return array all other caps used in module |
f432bebf | 1563 | */ |
1564 | function quiz_get_extra_capabilities() { | |
d774e817 | 1565 | global $CFG; |
be0ba083 | 1566 | require_once($CFG->libdir.'/questionlib.php'); |
cca6e300 | 1567 | $caps = question_get_all_capabilities(); |
1568 | $caps[] = 'moodle/site:accessallgroups'; | |
1569 | return $caps; | |
f432bebf | 1570 | } |
55f599f0 | 1571 | |
1572 | /** | |
792881f0 | 1573 | * This fucntion extends the global navigation for the site. |
55f599f0 | 1574 | * It is important to note that you should not rely on PAGE objects within this |
1575 | * body of code as there is no guarantee that during an AJAX request they are | |
1576 | * available | |
1577 | * | |
56ed242b | 1578 | * @param navigation_node $quiznode The quiz node within the global navigation |
0ff4bd08 TH |
1579 | * @param object $course The course object returned from the DB |
1580 | * @param object $module The module object returned from the DB | |
1581 | * @param object $cm The course module instance returned from the DB | |
55f599f0 | 1582 | */ |
56ed242b SH |
1583 | function quiz_extend_navigation($quiznode, $course, $module, $cm) { |
1584 | global $CFG; | |
1585 | ||
1586 | $context = get_context_instance(CONTEXT_MODULE, $cm->id); | |
1587 | ||
1588 | if (has_capability('mod/quiz:view', $context)) { | |
1589 | $url = new moodle_url('/mod/quiz/view.php', array('id'=>$cm->id)); | |
a49cb927 TH |
1590 | $quiznode->add(get_string('info', 'quiz'), $url, navigation_node::TYPE_SETTING, |
1591 | null, null, new pix_icon('i/info', '')); | |
56ed242b SH |
1592 | } |
1593 | ||
449bfe90 | 1594 | if (has_any_capability(array('mod/quiz:viewreports', 'mod/quiz:grade'), $context)) { |
56ed242b SH |
1595 | require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php'); |
1596 | $reportlist = quiz_report_list($context); | |
a49cb927 | 1597 | |
25a03faa TH |
1598 | $url = new moodle_url('/mod/quiz/report.php', |
1599 | array('id' => $cm->id, 'mode' => reset($reportlist))); | |
1600 | $reportnode = $quiznode->add(get_string('results', 'quiz'), $url, | |
1601 | navigation_node::TYPE_SETTING, | |
a49cb927 TH |
1602 | null, null, new pix_icon('i/report', '')); |
1603 | ||
56ed242b | 1604 | foreach ($reportlist as $report) { |
25a03faa TH |
1605 | $url = new moodle_url('/mod/quiz/report.php', |
1606 | array('id' => $cm->id, 'mode' => $report)); | |
1607 | $reportnode->add(get_string($report, 'quiz_'.$report), $url, | |
1608 | navigation_node::TYPE_SETTING, | |
2a8a78c3 | 1609 | null, 'quiz_report_' . $report, new pix_icon('i/item', '')); |
56ed242b SH |
1610 | } |
1611 | } | |
55f599f0 | 1612 | } |
1613 | ||
1614 | /** | |
1615 | * This function extends the settings navigation block for the site. | |
1616 | * | |
1617 | * It is safe to rely on PAGE here as we will only ever be within the module | |
1618 | * context when this is called | |
1619 | * | |
0b29477b SH |
1620 | * @param settings_navigation $settings |
1621 | * @param navigation_node $quiznode | |
55f599f0 | 1622 | */ |
0b29477b SH |
1623 | function quiz_extend_settings_navigation($settings, $quiznode) { |
1624 | global $PAGE, $CFG; | |
55f599f0 | 1625 | |
9e83f3d1 TH |
1626 | // Require {@link questionlib.php} |
1627 | // Included here as we only ever want to include this file if we really need to. | |
56ed242b | 1628 | require_once($CFG->libdir . '/questionlib.php'); |
55f599f0 | 1629 | |
bc502c16 TH |
1630 | // We want to add these new nodes after the Edit settings node, and before the |
1631 | // Locally assigned roles node. Of course, both of those are controlled by capabilities. | |
1632 | $keys = $quiznode->get_children_key_list(); | |
1b8655ab | 1633 | $beforekey = null; |
bc502c16 | 1634 | $i = array_search('modedit', $keys); |
1b8655ab | 1635 | if ($i === false and array_key_exists(0, $keys)) { |
bc502c16 TH |
1636 | $beforekey = $keys[0]; |
1637 | } else if (array_key_exists($i + 1, $keys)) { | |
1638 | $beforekey = $keys[$i + 1]; | |
bc502c16 TH |
1639 | } |
1640 | ||
56ed242b | 1641 | if (has_capability('mod/quiz:manageoverrides', $PAGE->cm->context)) { |
56ed242b | 1642 | $url = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$PAGE->cm->id)); |
bc502c16 | 1643 | $node = navigation_node::create(get_string('groupoverrides', 'quiz'), |
25a03faa | 1644 | new moodle_url($url, array('mode'=>'group')), |
bc502c16 TH |
1645 | navigation_node::TYPE_SETTING, null, 'mod_quiz_groupoverrides'); |
1646 | $quiznode->add_node($node, $beforekey); | |
1647 | ||
1648 | $node = navigation_node::create(get_string('useroverrides', 'quiz'), | |
25a03faa | 1649 | new moodle_url($url, array('mode'=>'user')), |
bc502c16 TH |
1650 | navigation_node::TYPE_SETTING, null, 'mod_quiz_useroverrides'); |
1651 | $quiznode->add_node($node, $beforekey); | |
55f599f0 | 1652 | } |
56ed242b | 1653 | |
55f599f0 | 1654 | if (has_capability('mod/quiz:manage', $PAGE->cm->context)) { |
bc502c16 TH |
1655 | $node = navigation_node::create(get_string('editquiz', 'quiz'), |
1656 | new moodle_url('/mod/quiz/edit.php', array('cmid'=>$PAGE->cm->id)), | |
1657 | navigation_node::TYPE_SETTING, null, 'mod_quiz_edit', | |
1658 | new pix_icon('t/edit', '')); | |
1659 | $quiznode->add_node($node, $beforekey); | |
55f599f0 | 1660 | } |
56ed242b SH |
1661 | |
1662 | if (has_capability('mod/quiz:preview', $PAGE->cm->context)) { | |
25a03faa TH |
1663 | $url = new moodle_url('/mod/quiz/startattempt.php', |
1664 | array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey())); | |
bc502c16 TH |
1665 | $node = navigation_node::create(get_string('preview', 'quiz'), $url, |
1666 | navigation_node::TYPE_SETTING, null, 'mod_quiz_preview', | |
1667 | new pix_icon('t/preview', '')); | |
1668 | $quiznode->add_node($node, $beforekey); | |
55f599f0 | 1669 | } |
56ed242b | 1670 | |
2a8a78c3 | 1671 | question_extend_settings_navigation($quiznode, $PAGE->cm->context)->trim_if_empty(); |
56ed242b | 1672 | } |
fe6ce234 DC |
1673 | |
1674 | /** | |
1675 | * Serves the quiz files. | |
1676 | * | |
d2b7803e DC |
1677 | * @package mod_quiz |
1678 | * @category files | |
1679 | * @param stdClass $course course object | |
1680 | * @param stdClass $cm course module object | |
1681 | * @param stdClass $context context object | |
1682 | * @param string $filearea file area | |
1683 | * @param array $args extra arguments | |
1684 | * @param bool $forcedownload whether or not force download | |
261cbbac | 1685 | * @param array $options additional options affecting the file serving |
fe6ce234 DC |
1686 | * @return bool false if file not found, does not return if found - justsend the file |
1687 | */ | |
261cbbac | 1688 | function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { |
fe6ce234 DC |
1689 | global $CFG, $DB; |
1690 | ||
1691 | if ($context->contextlevel != CONTEXT_MODULE) { | |
1692 | return false; | |
1693 | } | |
1694 | ||
1695 | require_login($course, false, $cm); | |
1696 | ||
1697 | if (!$quiz = $DB->get_record('quiz', array('id'=>$cm->instance))) { | |
1698 | return false; | |
1699 | } | |
1700 | ||
9e83f3d1 | 1701 | // The 'intro' area is served by pluginfile.php. |
fe6ce234 DC |
1702 | $fileareas = array('feedback'); |
1703 | if (!in_array($filearea, $fileareas)) { | |
1704 | return false; | |
1705 | } | |
1706 | ||
1707 | $feedbackid = (int)array_shift($args); | |
1708 | if (!$feedback = $DB->get_record('quiz_feedback', array('id'=>$feedbackid))) { | |
1709 | return false; | |
1710 | } | |
1711 | ||
1712 | $fs = get_file_storage(); | |
1713 | $relativepath = implode('/', $args); | |
1714 | $fullpath = "/$context->id/mod_quiz/$filearea/$feedbackid/$relativepath"; | |
1715 | if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { | |
1716 | return false; | |
1717 | } | |
261cbbac | 1718 | send_stored_file($file, 0, 0, true, $options); |
fe6ce234 DC |
1719 | } |
1720 | ||
1721 | /** | |
1722 | * Called via pluginfile.php -> question_pluginfile to serve files belonging to | |
1723 | * a question in a question_attempt when that attempt is a quiz attempt. | |
1724 | * | |
d2b7803e DC |
1725 | * @package mod_quiz |
1726 | * @category files | |
1727 | * @param stdClass $course course settings object | |
1728 | * @param stdClass $context context object | |
fe6ce234 DC |
1729 | * @param string $component the name of the component we are serving files for. |
1730 | * @param string $filearea the name of the file area. | |
d2b7803e DC |
1731 | * @param int $qubaid the attempt usage id. |
1732 | * @param int $slot the id of a question in this quiz attempt. | |
fe6ce234 DC |
1733 | * @param array $args the remaining bits of the file path. |
1734 | * @param bool $forcedownload whether the user must be forced to download the file. | |
261cbbac | 1735 | * @param array $options additional options affecting the file serving |
fe6ce234 DC |
1736 | * @return bool false if file not found, does not return if found - justsend the file |
1737 | */ | |
56e82d99 | 1738 | function mod_quiz_question_pluginfile($course, $context, $component, |
261cbbac | 1739 | $filearea, $qubaid, $slot, $args, $forcedownload, array $options=array()) { |
cef18275 | 1740 | global $CFG; |
fe6ce234 DC |
1741 | require_once($CFG->dirroot . '/mod/quiz/locallib.php'); |
1742 | ||
56e82d99 | 1743 | $attemptobj = quiz_attempt::create_from_usage_id($qubaid); |
cdbea7ee | 1744 | require_login($attemptobj->get_course(), false, $attemptobj->get_cm()); |
fe6ce234 DC |
1745 | |
1746 | if ($attemptobj->is_own_attempt() && !$attemptobj->is_finished()) { | |
1747 | // In the middle of an attempt. | |
1748 | if (!$attemptobj->is_preview_user()) { | |
1749 | $attemptobj->require_capability('mod/quiz:attempt'); | |
1750 | } | |
1751 | $isreviewing = false; | |
1752 | ||
1753 | } else { | |
1754 | // Reviewing an attempt. | |
1755 | $attemptobj->check_review_capability(); | |
1756 | $isreviewing = true; | |
1757 | } | |
1758 | ||
56e82d99 | 1759 | if (!$attemptobj->check_file_access($slot, $isreviewing, $context->id, |
fe6ce234 DC |
1760 | $component, $filearea, $args, $forcedownload)) { |
1761 | send_file_not_found(); | |
1762 | } | |
1763 | ||
1764 | $fs = get_file_storage(); | |
1765 | $relativepath = implode('/', $args); | |
1766 | $fullpath = "/$context->id/$component/$filearea/$relativepath"; | |
1767 | if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { | |
1768 | send_file_not_found(); | |
1769 | } | |
1770 | ||
261cbbac | 1771 | send_stored_file($file, 0, 0, $forcedownload, $options); |
fe6ce234 | 1772 | } |
b1627a92 DC |
1773 | |
1774 | /** | |
1775 | * Return a list of page types | |
1776 | * @param string $pagetype current page type | |
1777 | * @param stdClass $parentcontext Block's parent context | |
1778 | * @param stdClass $currentcontext Current context of block | |
1779 | */ | |
b38e2e28 | 1780 | function quiz_page_type_list($pagetype, $parentcontext, $currentcontext) { |
346a32a7 AD |
1781 | $module_pagetype = array( |
1782 | 'mod-quiz-*'=>get_string('page-mod-quiz-x', 'quiz'), | |
1783 | 'mod-quiz-edit'=>get_string('page-mod-quiz-edit', 'quiz')); | |
b1627a92 DC |
1784 | return $module_pagetype; |
1785 | } | |
33c8d37b CF |
1786 | |
1787 | /** | |
1788 | * @return the options for quiz navigation. | |
1789 | */ | |
1790 | function quiz_get_navigation_options() { | |
1791 | return array( | |
1792 | QUIZ_NAVMETHOD_FREE => get_string('navmethod_free', 'quiz'), | |
1793 | QUIZ_NAVMETHOD_SEQ => get_string('navmethod_seq', 'quiz') | |
1794 | ); | |
1795 | } |