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