d1290cec |
1 | <?php // $Id$ |
ee1fb969 |
2 | /** |
3 | * Library of functions for the quiz module. |
4 | * |
5 | * This contains functions that are called also from outside the quiz module |
6 | * Functions that are only called by the quiz module itself are in {@link locallib.php} |
ee1fb969 |
7 | * @author Martin Dougiamas and many others. |
8 | * @license http://www.gnu.org/copyleft/gpl.html GNU Public License |
9 | * @package quiz |
10 | */ |
730fd187 |
11 | |
c4d588cc |
12 | require_once($CFG->libdir.'/pagelib.php'); |
f67172b6 |
13 | require_once($CFG->libdir.'/questionlib.php'); |
8966a111 |
14 | |
75cd257b |
15 | /// CONSTANTS /////////////////////////////////////////////////////////////////// |
16 | |
17 | /**#@+ |
18 | * The different review options are stored in the bits of $quiz->review |
19 | * These constants help to extract the options |
b159da78 |
20 | * |
00719c02 |
21 | * This is more of a mess than you might think necessary, because originally |
22 | * it was though that 3x6 bits were enough, but then they ran out. PHP integers |
23 | * are only reliably 32 bits signed, so the simplest solution was then to |
b159da78 |
24 | * add 4x3 more bits. |
75cd257b |
25 | */ |
26 | /** |
00719c02 |
27 | * The first 6 + 4 bits refer to the time immediately after the attempt |
75cd257b |
28 | */ |
00719c02 |
29 | define('QUIZ_REVIEW_IMMEDIATELY', 0x3c003f); |
75cd257b |
30 | /** |
00719c02 |
31 | * the next 6 + 4 bits refer to the time after the attempt but while the quiz is open |
75cd257b |
32 | */ |
00719c02 |
33 | define('QUIZ_REVIEW_OPEN', 0x3c00fc0); |
75cd257b |
34 | /** |
00719c02 |
35 | * the final 6 + 4 bits refer to the time after the quiz closes |
75cd257b |
36 | */ |
00719c02 |
37 | define('QUIZ_REVIEW_CLOSED', 0x3c03f000); |
75cd257b |
38 | |
39 | // within each group of 6 bits we determine what should be shown |
00719c02 |
40 | define('QUIZ_REVIEW_RESPONSES', 1*0x1041); // Show responses |
41 | define('QUIZ_REVIEW_SCORES', 2*0x1041); // Show scores |
42 | define('QUIZ_REVIEW_FEEDBACK', 4*0x1041); // Show question feedback |
43 | define('QUIZ_REVIEW_ANSWERS', 8*0x1041); // Show correct answers |
75cd257b |
44 | // Some handling of worked solutions is already in the code but not yet fully supported |
45 | // and not switched on in the user interface. |
00719c02 |
46 | define('QUIZ_REVIEW_SOLUTIONS', 16*0x1041); // Show solutions |
47 | define('QUIZ_REVIEW_GENERALFEEDBACK',32*0x1041); // Show question general feedback |
48 | define('QUIZ_REVIEW_OVERALLFEEDBACK', 1*0x4440000); // Show quiz overall feedback |
49 | // Multipliers 2*0x4440000, 4*0x4440000 and 8*0x4440000 are still available |
75cd257b |
50 | /**#@-*/ |
51 | |
52 | /** |
53 | * If start and end date for the quiz are more than this many seconds apart |
54 | * they will be represented by two separate events in the calendar |
55 | */ |
00719c02 |
56 | define("QUIZ_MAX_EVENT_LENGTH", 5*24*60*60); // 5 days maximum |
ee1fb969 |
57 | |
a5e1f35c |
58 | /// FUNCTIONS /////////////////////////////////////////////////////////////////// |
730fd187 |
59 | |
920b93d1 |
60 | /** |
61 | * Given an object containing all the necessary data, |
62 | * (defined by the form in mod.html) this function |
63 | * will create a new instance and return the id number |
64 | * of the new instance. |
a23f0aaf |
65 | * |
920b93d1 |
66 | * @param object $quiz the data that came from the form. |
212b7b8c |
67 | * @return mixed the id of the new instance on success, |
68 | * false or a string error message on failure. |
920b93d1 |
69 | */ |
730fd187 |
70 | function quiz_add_instance($quiz) { |
730fd187 |
71 | |
920b93d1 |
72 | // Process the options from the form. |
73 | $quiz->created = time(); |
bc569413 |
74 | $quiz->questions = ''; |
212b7b8c |
75 | $result = quiz_process_options($quiz); |
76 | if ($result && is_string($result)) { |
77 | return $result; |
78 | } |
6f797013 |
79 | |
920b93d1 |
80 | // Try to store it in the database. |
81 | if (!$quiz->id = insert_record("quiz", $quiz)) { |
82 | return false; |
34283aa8 |
83 | } |
7bd1aa1d |
84 | |
920b93d1 |
85 | // Do the processing required after an add or an update. |
86 | quiz_after_add_or_update($quiz); |
a23f0aaf |
87 | |
7bd1aa1d |
88 | return $quiz->id; |
730fd187 |
89 | } |
90 | |
920b93d1 |
91 | /** |
92 | * Given an object containing all the necessary data, |
93 | * (defined by the form in mod.html) this function |
94 | * will update an existing instance with new data. |
a23f0aaf |
95 | * |
920b93d1 |
96 | * @param object $quiz the data that came from the form. |
212b7b8c |
97 | * @return mixed true on success, false or a string error message on failure. |
920b93d1 |
98 | */ |
730fd187 |
99 | function quiz_update_instance($quiz) { |
730fd187 |
100 | |
920b93d1 |
101 | // Process the options from the form. |
212b7b8c |
102 | $result = quiz_process_options($quiz); |
103 | if ($result && is_string($result)) { |
104 | return $result; |
105 | } |
ee1fb969 |
106 | |
920b93d1 |
107 | // Update the database. |
730fd187 |
108 | $quiz->id = $quiz->instance; |
7bd1aa1d |
109 | if (!update_record("quiz", $quiz)) { |
110 | return false; // some error occurred |
111 | } |
730fd187 |
112 | |
920b93d1 |
113 | // Do the processing required after an add or an update. |
114 | quiz_after_add_or_update($quiz); |
ee1fb969 |
115 | |
920b93d1 |
116 | // Delete any previous preview attempts |
117 | delete_records('quiz_attempts', 'preview', '1', 'quiz', $quiz->id); |
d2f308c0 |
118 | |
7bd1aa1d |
119 | return true; |
730fd187 |
120 | } |
121 | |
122 | |
123 | function quiz_delete_instance($id) { |
f41e824f |
124 | /// Given an ID of an instance of this module, |
125 | /// this function will permanently delete the instance |
126 | /// and any data that depends on it. |
730fd187 |
127 | |
128 | if (! $quiz = get_record("quiz", "id", "$id")) { |
129 | return false; |
130 | } |
131 | |
132 | $result = true; |
133 | |
464edd6d |
134 | if ($attempts = get_records("quiz_attempts", "quiz", "$quiz->id")) { |
10b9291c |
135 | foreach ($attempts as $attempt) { |
212b7b8c |
136 | // TODO: this should use the delete_attempt($attempt->uniqueid) function in questionlib.php |
4f48fb42 |
137 | if (! delete_records("question_states", "attempt", "$attempt->uniqueid")) { |
d115d8c7 |
138 | $result = false; |
139 | } |
03d1753c |
140 | if (! delete_records("question_sessions", "attemptid", "$attempt->uniqueid")) { |
10b9291c |
141 | $result = false; |
708b521a |
142 | } |
10b9291c |
143 | } |
144 | } |
145 | |
212b7b8c |
146 | $tables_to_purge = array( |
147 | 'quiz_attempts' => 'quiz', |
148 | 'quiz_grades' => 'quiz', |
149 | 'quiz_question_instances' => 'quiz', |
150 | 'quiz_grades' => 'quiz', |
151 | 'quiz_feedback' => 'quizid', |
152 | 'quiz' => 'id' |
153 | ); |
154 | foreach ($tables_to_purge as $table => $keyfield) { |
155 | if (!delete_records($table, $keyfield, $quiz->id)) { |
156 | $result = false; |
157 | } |
730fd187 |
158 | } |
159 | |
880d8675 |
160 | $pagetypes = page_import_types('mod/quiz/'); |
161 | foreach($pagetypes as $pagetype) { |
162 | if(!delete_records('block_instance', 'pageid', $quiz->id, 'pagetype', $pagetype)) { |
163 | $result = false; |
164 | } |
165 | } |
166 | |
78036f78 |
167 | if ($events = get_records_select('event', "modulename = 'quiz' and instance = '$quiz->id'")) { |
168 | foreach($events as $event) { |
169 | delete_event($event->id); |
170 | } |
b2a3cd2d |
171 | } |
172 | |
d6dd2108 |
173 | quiz_grade_item_delete($quiz); |
174 | |
730fd187 |
175 | return $result; |
176 | } |
177 | |
b2d594c8 |
178 | |
730fd187 |
179 | function quiz_user_outline($course, $user, $mod, $quiz) { |
f41e824f |
180 | /// Return a small object with summary information about what a |
a5e1f35c |
181 | /// user has done with a given particular instance of this module |
182 | /// Used for user activity reports. |
183 | /// $return->time = the time they did it |
184 | /// $return->info = a short text description |
9d677360 |
185 | if ($grade = get_record('quiz_grades', 'userid', $user->id, 'quiz', $quiz->id)) { |
a23f0aaf |
186 | |
ce687025 |
187 | $result = new stdClass; |
090cf95a |
188 | if ((float)$grade->grade) { |
1105b32d |
189 | $result->info = get_string('grade').': '.round($grade->grade, $quiz->decimalpoints); |
98092498 |
190 | } |
191 | $result->time = $grade->timemodified; |
192 | return $result; |
193 | } |
194 | return NULL; |
730fd187 |
195 | |
730fd187 |
196 | } |
197 | |
ee1fb969 |
198 | |
730fd187 |
199 | function quiz_user_complete($course, $user, $mod, $quiz) { |
f41e824f |
200 | /// Print a detailed representation of what a user has done with |
a5e1f35c |
201 | /// a given particular instance of this module, for user activity reports. |
730fd187 |
202 | |
ee1fb969 |
203 | if ($attempts = get_records_select('quiz_attempts', "userid='$user->id' AND quiz='$quiz->id'", 'attempt ASC')) { |
8779eab5 |
204 | if ($quiz->grade and $quiz->sumgrades && $grade = get_record('quiz_grades', 'userid', $user->id, 'quiz', $quiz->id)) { |
1105b32d |
205 | echo get_string('grade').': '.round($grade->grade, $quiz->decimalpoints).'/'.$quiz->grade.'<br />'; |
ee1fb969 |
206 | } |
207 | foreach ($attempts as $attempt) { |
208 | echo get_string('attempt', 'quiz').' '.$attempt->attempt.': '; |
209 | if ($attempt->timefinish == 0) { |
210 | print_string('unfinished'); |
211 | } else { |
1105b32d |
212 | echo round($attempt->sumgrades, $quiz->decimalpoints).'/'.$quiz->sumgrades; |
ee1fb969 |
213 | } |
214 | echo ' - '.userdate($attempt->timemodified).'<br />'; |
215 | } |
216 | } else { |
217 | print_string('noattempts', 'quiz'); |
218 | } |
219 | |
730fd187 |
220 | return true; |
221 | } |
222 | |
ee1fb969 |
223 | |
730fd187 |
224 | function quiz_cron () { |
a5e1f35c |
225 | /// Function to be run periodically according to the moodle cron |
f41e824f |
226 | /// This function searches for things that need to be done, such |
227 | /// as sending out mail, toggling flags etc ... |
730fd187 |
228 | |
229 | global $CFG; |
230 | |
231 | return true; |
232 | } |
233 | |
858deff0 |
234 | |
d6dd2108 |
235 | /** |
236 | * Return grade for given user or all users. |
237 | * |
238 | * @param int $quizid id of quiz |
239 | * @param int $userid optional user id, 0 means all users |
240 | * @return array array of grades, false if none |
241 | */ |
242 | function quiz_get_user_grades($quiz, $userid=0) { |
243 | global $CFG; |
244 | |
245 | $user = $userid ? "AND u.id = $userid" : ""; |
246 | |
ac9b0805 |
247 | $sql = "SELECT u.id, u.id AS userid, g.grade AS rawgrade |
d6dd2108 |
248 | FROM {$CFG->prefix}user u, {$CFG->prefix}quiz_grades g |
249 | WHERE u.id = g.userid AND g.quiz = $quiz->id |
250 | $user"; |
251 | |
252 | return get_records_sql($sql); |
253 | } |
254 | |
255 | /** |
256 | * Update grades in central gradebook |
257 | * |
258 | * @param object $quiz null means all quizs |
259 | * @param int $userid specific user only, 0 mean all |
260 | */ |
261 | function quiz_update_grades($quiz=null, $userid=0, $nullifnone=true) { |
262 | global $CFG; |
263 | if (!function_exists('grade_update')) { //workaround for buggy PHP versions |
264 | require_once($CFG->libdir.'/gradelib.php'); |
ed1daaa9 |
265 | } |
266 | |
d6dd2108 |
267 | if ($quiz != null) { |
f4d24ff5 |
268 | quiz_grade_item_update($quiz); // Recreate it if necessary |
d6dd2108 |
269 | if ($grades = quiz_get_user_grades($quiz, $userid)) { |
270 | grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades); |
271 | |
272 | } else if ($userid and $nullifnone) { |
273 | $grade = new object(); |
31f626f8 |
274 | $grade->userid = $userid; |
ac9b0805 |
275 | $grade->rawgrade = NULL; |
d6dd2108 |
276 | grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grade); |
277 | } |
278 | |
279 | } else { |
280 | $sql = "SELECT a.*, cm.idnumber as cmidnumber, a.course as courseid |
281 | FROM {$CFG->prefix}quiz a, {$CFG->prefix}course_modules cm, {$CFG->prefix}modules m |
282 | WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id"; |
283 | if ($rs = get_recordset_sql($sql)) { |
284 | if ($rs->RecordCount() > 0) { |
285 | while ($quiz = rs_fetch_next_record($rs)) { |
286 | quiz_grade_item_update($quiz); |
287 | if ($quiz->grade != 0) { |
288 | quiz_update_grades($quiz, 0, false); |
289 | } |
290 | } |
291 | } |
292 | rs_close($rs); |
293 | } |
294 | } |
d0ac6bc2 |
295 | } |
296 | |
d6dd2108 |
297 | /** |
298 | * Create grade item for given quiz |
299 | * |
300 | * @param object $quiz object with extra cmidnumber |
301 | * @return int 0 if ok, error code otherwise |
302 | */ |
303 | function quiz_grade_item_update($quiz) { |
304 | global $CFG; |
305 | if (!function_exists('grade_update')) { //workaround for buggy PHP versions |
306 | require_once($CFG->libdir.'/gradelib.php'); |
307 | } |
308 | |
309 | if (array_key_exists('cmidnumber', $quiz)) { //it may not be always present |
310 | $params = array('itemname'=>$quiz->name, 'idnumber'=>$quiz->cmidnumber); |
311 | } else { |
312 | $params = array('itemname'=>$quiz->name); |
313 | } |
314 | |
315 | if ($quiz->grade > 0) { |
316 | $params['gradetype'] = GRADE_TYPE_VALUE; |
317 | $params['grademax'] = $quiz->grade; |
318 | $params['grademin'] = 0; |
319 | |
320 | } else { |
321 | $params['gradetype'] = GRADE_TYPE_NONE; |
322 | } |
323 | |
324 | return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, NULL, $params); |
325 | } |
326 | |
327 | /** |
328 | * Delete grade item for given quiz |
329 | * |
330 | * @param object $quiz object |
331 | * @return object quiz |
332 | */ |
333 | function quiz_grade_item_delete($quiz) { |
334 | global $CFG; |
335 | require_once($CFG->libdir.'/gradelib.php'); |
336 | |
337 | return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, NULL, array('deleted'=>1)); |
338 | } |
339 | |
340 | |
d061d883 |
341 | function quiz_get_participants($quizid) { |
342 | /// Returns an array of users who have data in a given quiz |
e4acc4ce |
343 | /// (users with records in quiz_attempts and quiz_question_versions) |
d061d883 |
344 | |
345 | global $CFG; |
346 | |
e4acc4ce |
347 | //Get users from attempts |
348 | $us_attempts = get_records_sql("SELECT DISTINCT u.id, u.id |
349 | FROM {$CFG->prefix}user u, |
350 | {$CFG->prefix}quiz_attempts a |
351 | WHERE a.quiz = '$quizid' and |
352 | u.id = a.userid"); |
353 | |
354 | //Get users from question_versions |
355 | $us_versions = get_records_sql("SELECT DISTINCT u.id, u.id |
356 | FROM {$CFG->prefix}user u, |
19098759 |
357 | {$CFG->prefix}quiz_question_versions v |
e4acc4ce |
358 | WHERE v.quiz = '$quizid' and |
359 | u.id = v.userid"); |
360 | |
361 | //Add us_versions to us_attempts |
362 | if ($us_versions) { |
363 | foreach ($us_versions as $us_version) { |
364 | $us_attempts[$us_version->id] = $us_version; |
365 | } |
366 | } |
367 | //Return us_attempts array (it contains an array of unique users) |
368 | return ($us_attempts); |
369 | |
d061d883 |
370 | } |
730fd187 |
371 | |
d2f308c0 |
372 | function quiz_refresh_events($courseid = 0) { |
920b93d1 |
373 | // This horrible function only seems to be called from mod/quiz/db/[dbtype].php. |
374 | |
d2f308c0 |
375 | // This standard function will check all instances of this module |
376 | // and make sure there are up-to-date events created for each of them. |
b2a3cd2d |
377 | // If courseid = 0, then every quiz event in the site is checked, else |
378 | // only quiz events belonging to the course specified are checked. |
d2f308c0 |
379 | // This function is used, in its new format, by restore_refresh_events() |
380 | |
381 | if ($courseid == 0) { |
382 | if (! $quizzes = get_records("quiz")) { |
383 | return true; |
384 | } |
385 | } else { |
386 | if (! $quizzes = get_records("quiz", "course", $courseid)) { |
387 | return true; |
388 | } |
389 | } |
dcd338ff |
390 | $moduleid = get_field('modules', 'id', 'name', 'quiz'); |
f41e824f |
391 | |
d2f308c0 |
392 | foreach ($quizzes as $quiz) { |
393 | $event = NULL; |
b2a3cd2d |
394 | $event2 = NULL; |
395 | $event2old = NULL; |
396 | |
397 | if ($events = get_records_select('event', "modulename = 'quiz' AND instance = '$quiz->id' ORDER BY timestart")) { |
398 | $event = array_shift($events); |
399 | if (!empty($events)) { |
400 | $event2old = array_shift($events); |
401 | if (!empty($events)) { |
402 | foreach ($events as $badevent) { |
403 | delete_records('event', 'id', $badevent->id); |
404 | } |
405 | } |
406 | } |
407 | } |
408 | |
d2f308c0 |
409 | $event->name = addslashes($quiz->name); |
410 | $event->description = addslashes($quiz->intro); |
b2a3cd2d |
411 | $event->courseid = $quiz->course; |
412 | $event->groupid = 0; |
413 | $event->userid = 0; |
414 | $event->modulename = 'quiz'; |
415 | $event->instance = $quiz->id; |
ba288539 |
416 | $event->visible = instance_is_visible('quiz', $quiz); |
d2f308c0 |
417 | $event->timestart = $quiz->timeopen; |
b2a3cd2d |
418 | $event->eventtype = 'open'; |
d2f308c0 |
419 | $event->timeduration = ($quiz->timeclose - $quiz->timeopen); |
d2f308c0 |
420 | |
b2a3cd2d |
421 | if ($event->timeduration > QUIZ_MAX_EVENT_LENGTH) { /// Set up two events |
d2f308c0 |
422 | |
b2a3cd2d |
423 | $event2 = $event; |
d2f308c0 |
424 | |
b2a3cd2d |
425 | $event->name = addslashes($quiz->name).' ('.get_string('quizopens', 'quiz').')'; |
426 | $event->timeduration = 0; |
427 | |
428 | $event2->name = addslashes($quiz->name).' ('.get_string('quizcloses', 'quiz').')'; |
429 | $event2->timestart = $quiz->timeclose; |
430 | $event2->eventtype = 'close'; |
431 | $event2->timeduration = 0; |
432 | |
433 | if (empty($event2old->id)) { |
434 | unset($event2->id); |
435 | add_event($event2); |
436 | } else { |
437 | $event2->id = $event2old->id; |
438 | update_event($event2); |
439 | } |
acda04bf |
440 | } else if (!empty($event2old->id)) { |
441 | delete_event($event2old->id); |
b2a3cd2d |
442 | } |
443 | |
444 | if (empty($event->id)) { |
acda04bf |
445 | if (!empty($event->timestart)) { |
446 | add_event($event); |
447 | } |
b2a3cd2d |
448 | } else { |
449 | update_event($event); |
d2f308c0 |
450 | } |
b2a3cd2d |
451 | |
d2f308c0 |
452 | } |
453 | return true; |
454 | } |
455 | |
6710ec87 |
456 | |
457 | function quiz_get_recent_mod_activity(&$activities, &$index, $sincetime, $courseid, $quiz="0", $user="", $groupid="") { |
458 | // Returns all quizzes since a given time. If quiz is specified then |
459 | // this restricts the results |
460 | |
461 | global $CFG; |
462 | |
463 | if ($quiz) { |
464 | $quizselect = " AND cm.id = '$quiz'"; |
465 | } else { |
466 | $quizselect = ""; |
467 | } |
468 | if ($user) { |
469 | $userselect = " AND u.id = '$user'"; |
470 | } else { |
471 | $userselect = ""; |
472 | } |
473 | |
474 | $quizzes = get_records_sql("SELECT qa.*, q.name, u.firstname, u.lastname, u.picture, |
475 | q.course, q.sumgrades as maxgrade, cm.instance, cm.section |
476 | FROM {$CFG->prefix}quiz_attempts qa, |
477 | {$CFG->prefix}quiz q, |
478 | {$CFG->prefix}user u, |
479 | {$CFG->prefix}course_modules cm |
480 | WHERE qa.timefinish > '$sincetime' |
481 | AND qa.userid = u.id $userselect |
482 | AND qa.quiz = q.id $quizselect |
483 | AND cm.instance = q.id |
484 | AND cm.course = '$courseid' |
485 | AND q.course = cm.course |
486 | ORDER BY qa.timefinish ASC"); |
487 | |
488 | if (empty($quizzes)) |
489 | return; |
490 | |
491 | foreach ($quizzes as $quiz) { |
f3f7610c |
492 | if (empty($groupid) || groups_is_member($groupid, $quiz->userid)) { |
6710ec87 |
493 | |
ac21ad39 |
494 | $tmpactivity = new Object; |
495 | |
6710ec87 |
496 | $tmpactivity->type = "quiz"; |
497 | $tmpactivity->defaultindex = $index; |
498 | $tmpactivity->instance = $quiz->quiz; |
499 | |
500 | $tmpactivity->name = $quiz->name; |
501 | $tmpactivity->section = $quiz->section; |
502 | |
503 | $tmpactivity->content->attemptid = $quiz->id; |
504 | $tmpactivity->content->sumgrades = $quiz->sumgrades; |
505 | $tmpactivity->content->maxgrade = $quiz->maxgrade; |
506 | $tmpactivity->content->attempt = $quiz->attempt; |
507 | |
508 | $tmpactivity->user->userid = $quiz->userid; |
509 | $tmpactivity->user->fullname = fullname($quiz); |
510 | $tmpactivity->user->picture = $quiz->picture; |
511 | |
512 | $tmpactivity->timestamp = $quiz->timefinish; |
513 | |
514 | $activities[] = $tmpactivity; |
515 | |
516 | $index++; |
517 | } |
518 | } |
519 | |
520 | return; |
521 | } |
522 | |
523 | |
524 | function quiz_print_recent_mod_activity($activity, $course, $detail=false) { |
6eaae5bd |
525 | global $CFG; |
6710ec87 |
526 | |
527 | echo '<table border="0" cellpadding="3" cellspacing="0">'; |
528 | |
6f797013 |
529 | echo "<tr><td class=\"forumpostpicture\" width=\"35\" valign=\"top\">"; |
6710ec87 |
530 | print_user_picture($activity->user->userid, $course, $activity->user->picture); |
09275894 |
531 | echo "</td><td style=\"width:100%;\"><font size=\"2\">"; |
6710ec87 |
532 | |
533 | if ($detail) { |
534 | echo "<img src=\"$CFG->modpixpath/$activity->type/icon.gif\" ". |
0d905d9f |
535 | "class=\"icon\" alt=\"$activity->type\" /> "; |
6710ec87 |
536 | echo "<a href=\"$CFG->wwwroot/mod/quiz/view.php?id=" . $activity->instance . "\">" |
95c67e2f |
537 | . format_string($activity->name,true) . "</a> - "; |
6710ec87 |
538 | |
539 | } |
540 | |
f5e9cb1e |
541 | if (has_capability('mod/quiz:grade', get_context_instance(CONTEXT_MODULE, $activity->instance))) { |
6710ec87 |
542 | $grades = "(" . $activity->content->sumgrades . " / " . $activity->content->maxgrade . ") "; |
543 | echo "<a href=\"$CFG->wwwroot/mod/quiz/review.php?q=" |
544 | . $activity->instance . "&attempt=" |
545 | . $activity->content->attemptid . "\">" . $grades . "</a> "; |
546 | |
547 | echo get_string("attempt", "quiz") . " - " . $activity->content->attempt . "<br />"; |
548 | } |
549 | echo "<a href=\"$CFG->wwwroot/user/view.php?id=" |
550 | . $activity->user->userid . "&course=$course\">" |
551 | . $activity->user->fullname . "</a> "; |
552 | |
553 | echo " - " . userdate($activity->timestamp); |
554 | |
555 | echo "</font></td></tr>"; |
556 | echo "</table>"; |
557 | |
558 | return; |
559 | } |
560 | |
ee1fb969 |
561 | /** |
920b93d1 |
562 | * Pre-process the quiz options form data, making any necessary adjustments. |
a0807a00 |
563 | * Called by add/update instance in this file, and the save code in admin/module.php. |
b159da78 |
564 | * |
920b93d1 |
565 | * @param object $quiz The variables set on the form. |
566 | */ |
567 | function quiz_process_options(&$quiz) { |
568 | $quiz->timemodified = time(); |
ee1fb969 |
569 | |
920b93d1 |
570 | // Quiz open time. |
a23f0aaf |
571 | if (empty($quiz->timeopen)) { |
920b93d1 |
572 | $quiz->preventlate = 0; |
ee1fb969 |
573 | } |
574 | |
dc5c6851 |
575 | // Quiz name. |
576 | if (!empty($quiz->name)) { |
577 | $quiz->name = trim($quiz->name); |
578 | } |
a23f0aaf |
579 | |
920b93d1 |
580 | // Time limit. (Get rid of it if the checkbox was not ticked.) |
a23f0aaf |
581 | if (empty($quiz->timelimitenable)) { |
920b93d1 |
582 | $quiz->timelimit = 0; |
583 | } |
584 | $quiz->timelimit = round($quiz->timelimit); |
a23f0aaf |
585 | |
ab0a8ff2 |
586 | // Password field - different in form to stop browsers that remember passwords |
587 | // getting confused. |
588 | $quiz->password = $quiz->quizpassword; |
589 | unset($quiz->quizpassword); |
590 | |
212b7b8c |
591 | // Quiz feedback |
a0807a00 |
592 | if (isset($quiz->feedbacktext)) { |
593 | // Clean up the boundary text. |
594 | for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) { |
595 | if (empty($quiz->feedbacktext[$i])) { |
596 | $quiz->feedbacktext[$i] = ''; |
597 | } else { |
598 | $quiz->feedbacktext[$i] = trim($quiz->feedbacktext[$i]); |
599 | } |
212b7b8c |
600 | } |
b159da78 |
601 | |
a0807a00 |
602 | // Check the boundary value is a number or a percentage, and in range. |
603 | $i = 0; |
604 | while (!empty($quiz->feedbackboundaries[$i])) { |
605 | $boundary = trim($quiz->feedbackboundaries[$i]); |
606 | if (!is_numeric($boundary)) { |
607 | if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') { |
608 | $boundary = trim(substr($boundary, 0, -1)); |
609 | if (is_numeric($boundary)) { |
610 | $boundary = $boundary * $quiz->grade / 100.0; |
611 | } else { |
612 | return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1); |
613 | } |
212b7b8c |
614 | } |
615 | } |
a0807a00 |
616 | if ($boundary <= 0 || $boundary >= $quiz->grade) { |
617 | return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1); |
618 | } |
619 | if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) { |
620 | return get_string('feedbackerrororder', 'quiz', $i + 1); |
621 | } |
622 | $quiz->feedbackboundaries[$i] = $boundary; |
623 | $i += 1; |
212b7b8c |
624 | } |
a0807a00 |
625 | $numboundaries = $i; |
b159da78 |
626 | |
a0807a00 |
627 | // Check there is nothing in the remaining unused fields. |
628 | for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) { |
629 | if (!empty($quiz->feedbackboundaries[$i]) && trim($quiz->feedbackboundaries[$i]) != '') { |
630 | return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1); |
631 | } |
212b7b8c |
632 | } |
a0807a00 |
633 | for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) { |
634 | if (!empty($quiz->feedbacktext[$i]) && trim($quiz->feedbacktext[$i]) != '') { |
635 | return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1); |
636 | } |
212b7b8c |
637 | } |
a0807a00 |
638 | $quiz->feedbackboundaries[-1] = $quiz->grade + 1; // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade(). |
639 | $quiz->feedbackboundaries[$numboundaries] = 0; |
640 | $quiz->feedbackboundarycount = $numboundaries; |
212b7b8c |
641 | } |
a23f0aaf |
642 | |
920b93d1 |
643 | // Settings that get combined to go into the optionflags column. |
644 | $quiz->optionflags = 0; |
645 | if (!empty($quiz->adaptive)) { |
646 | $quiz->optionflags |= QUESTION_ADAPTIVE; |
647 | } |
648 | |
649 | // Settings that get combined to go into the review column. |
650 | $review = 0; |
651 | if (isset($quiz->responsesimmediately)) { |
ee1fb969 |
652 | $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY); |
920b93d1 |
653 | unset($quiz->responsesimmediately); |
ee1fb969 |
654 | } |
920b93d1 |
655 | if (isset($quiz->responsesopen)) { |
ee1fb969 |
656 | $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN); |
920b93d1 |
657 | unset($quiz->responsesopen); |
ee1fb969 |
658 | } |
920b93d1 |
659 | if (isset($quiz->responsesclosed)) { |
ee1fb969 |
660 | $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED); |
920b93d1 |
661 | unset($quiz->responsesclosed); |
ee1fb969 |
662 | } |
663 | |
920b93d1 |
664 | if (isset($quiz->scoreimmediately)) { |
ee1fb969 |
665 | $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY); |
920b93d1 |
666 | unset($quiz->scoreimmediately); |
ee1fb969 |
667 | } |
920b93d1 |
668 | if (isset($quiz->scoreopen)) { |
ee1fb969 |
669 | $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN); |
920b93d1 |
670 | unset($quiz->scoreopen); |
ee1fb969 |
671 | } |
920b93d1 |
672 | if (isset($quiz->scoreclosed)) { |
ee1fb969 |
673 | $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED); |
920b93d1 |
674 | unset($quiz->scoreclosed); |
ee1fb969 |
675 | } |
676 | |
920b93d1 |
677 | if (isset($quiz->feedbackimmediately)) { |
ee1fb969 |
678 | $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY); |
920b93d1 |
679 | unset($quiz->feedbackimmediately); |
ee1fb969 |
680 | } |
920b93d1 |
681 | if (isset($quiz->feedbackopen)) { |
ee1fb969 |
682 | $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN); |
920b93d1 |
683 | unset($quiz->feedbackopen); |
ee1fb969 |
684 | } |
920b93d1 |
685 | if (isset($quiz->feedbackclosed)) { |
ee1fb969 |
686 | $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED); |
920b93d1 |
687 | unset($quiz->feedbackclosed); |
ee1fb969 |
688 | } |
689 | |
920b93d1 |
690 | if (isset($quiz->answersimmediately)) { |
ee1fb969 |
691 | $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY); |
920b93d1 |
692 | unset($quiz->answersimmediately); |
ee1fb969 |
693 | } |
920b93d1 |
694 | if (isset($quiz->answersopen)) { |
ee1fb969 |
695 | $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN); |
920b93d1 |
696 | unset($quiz->answersopen); |
ee1fb969 |
697 | } |
920b93d1 |
698 | if (isset($quiz->answersclosed)) { |
ee1fb969 |
699 | $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED); |
920b93d1 |
700 | unset($quiz->answersclosed); |
ee1fb969 |
701 | } |
702 | |
920b93d1 |
703 | if (isset($quiz->solutionsimmediately)) { |
ee1fb969 |
704 | $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_IMMEDIATELY); |
920b93d1 |
705 | unset($quiz->solutionsimmediately); |
ee1fb969 |
706 | } |
920b93d1 |
707 | if (isset($quiz->solutionsopen)) { |
ee1fb969 |
708 | $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_OPEN); |
920b93d1 |
709 | unset($quiz->solutionsopen); |
ee1fb969 |
710 | } |
920b93d1 |
711 | if (isset($quiz->solutionsclosed)) { |
ee1fb969 |
712 | $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_CLOSED); |
920b93d1 |
713 | unset($quiz->solutionsclosed); |
ee1fb969 |
714 | } |
715 | |
a4514d91 |
716 | if (isset($quiz->generalfeedbackimmediately)) { |
717 | $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY); |
00719c02 |
718 | unset($quiz->generalfeedbackimmediately); |
1b8a7434 |
719 | } |
a4514d91 |
720 | if (isset($quiz->generalfeedbackopen)) { |
721 | $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_OPEN); |
00719c02 |
722 | unset($quiz->generalfeedbackopen); |
1b8a7434 |
723 | } |
a4514d91 |
724 | if (isset($quiz->generalfeedbackclosed)) { |
725 | $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_CLOSED); |
00719c02 |
726 | unset($quiz->generalfeedbackclosed); |
727 | } |
728 | |
729 | if (isset($quiz->overallfeedbackimmediately)) { |
730 | $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_IMMEDIATELY); |
731 | unset($quiz->overallfeedbackimmediately); |
732 | } |
733 | if (isset($quiz->overallfeedbackopen)) { |
734 | $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_OPEN); |
735 | unset($quiz->overallfeedbackopen); |
736 | } |
737 | if (isset($quiz->overallfeedbackclosed)) { |
738 | $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_CLOSED); |
739 | unset($quiz->overallfeedbackclosed); |
1b8a7434 |
740 | } |
741 | |
920b93d1 |
742 | $quiz->review = $review; |
743 | } |
744 | |
745 | /** |
746 | * This function is called at the end of quiz_add_instance |
747 | * and quiz_update_instance, to do the common processing. |
a23f0aaf |
748 | * |
920b93d1 |
749 | * @param object $quiz the quiz object. |
750 | */ |
751 | function quiz_after_add_or_update($quiz) { |
752 | |
212b7b8c |
753 | // Save the feedback |
754 | delete_records('quiz_feedback', 'quizid', $quiz->id); |
a23f0aaf |
755 | |
212b7b8c |
756 | for ($i = 0; $i <= $quiz->feedbackboundarycount; $i += 1) { |
757 | $feedback = new stdClass; |
758 | $feedback->quizid = $quiz->id; |
759 | $feedback->feedbacktext = $quiz->feedbacktext[$i]; |
760 | $feedback->mingrade = $quiz->feedbackboundaries[$i]; |
761 | $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1]; |
762 | if (!insert_record('quiz_feedback', $feedback, false)) { |
763 | return "Could not save quiz feedback."; |
764 | } |
765 | } |
766 | |
ee1fb969 |
767 | |
920b93d1 |
768 | // Update the events relating to this quiz. |
769 | // This is slightly inefficient, deleting the old events and creating new ones. However, |
770 | // there are at most two events, and this keeps the code simpler. |
771 | if ($events = get_records_select('event', "modulename = 'quiz' and instance = '$quiz->id'")) { |
772 | foreach($events as $event) { |
773 | delete_event($event->id); |
774 | } |
775 | } |
776 | |
777 | $event = new stdClass; |
778 | $event->description = $quiz->intro; |
779 | $event->courseid = $quiz->course; |
780 | $event->groupid = 0; |
781 | $event->userid = 0; |
782 | $event->modulename = 'quiz'; |
783 | $event->instance = $quiz->id; |
784 | $event->timestart = $quiz->timeopen; |
785 | $event->timeduration = $quiz->timeclose - $quiz->timeopen; |
786 | $event->visible = instance_is_visible('quiz', $quiz); |
787 | $event->eventtype = 'open'; |
788 | |
789 | if ($quiz->timeclose and $quiz->timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) { |
790 | // Single event for the whole quiz. |
791 | $event->name = $quiz->name; |
792 | add_event($event); |
793 | } else { |
794 | // Separate start and end events. |
795 | $event->timeduration = 0; |
796 | if ($quiz->timeopen) { |
797 | $event->name = $quiz->name.' ('.get_string('quizopens', 'quiz').')'; |
798 | add_event($event); |
799 | unset($event->id); // So we can use the same object for the close event. |
800 | } |
801 | if ($quiz->timeclose) { |
802 | $event->name = $quiz->name.' ('.get_string('quizcloses', 'quiz').')'; |
803 | $event->timestart = $quiz->timeclose; |
804 | $event->eventtype = 'close'; |
805 | add_event($event); |
806 | } |
807 | } |
d6dd2108 |
808 | |
809 | //update related grade item |
83d606dd |
810 | quiz_grade_item_update(stripslashes_recursive($quiz)); |
ee1fb969 |
811 | } |
812 | |
f3221af9 |
813 | function quiz_get_view_actions() { |
814 | return array('view','view all','report'); |
815 | } |
ee1fb969 |
816 | |
f3221af9 |
817 | function quiz_get_post_actions() { |
818 | return array('attempt','editquestions','review','submit'); |
819 | } |
ee1fb969 |
820 | |
f67172b6 |
821 | /** |
822 | * Returns an array of names of quizzes that use this question |
823 | * |
f67172b6 |
824 | * @param object $questionid |
825 | * @return array of strings |
826 | */ |
827 | function quiz_question_list_instances($questionid) { |
e8666d9a |
828 | global $CFG; |
829 | |
830 | // TODO: we should also consider other questions that are used by |
831 | // random questions in this quiz, but that is very hard. |
832 | |
833 | $sql = "SELECT q.id, q.name |
834 | FROM {$CFG->prefix}quiz q |
835 | INNER JOIN |
836 | {$CFG->prefix}quiz_question_instances qqi |
837 | ON q.id = qqi.quiz |
838 | WHERE qqi.question = '$questionid'"; |
839 | |
840 | if ($instances = get_records_sql_menu($sql)) { |
841 | return $instances; |
842 | } |
f67172b6 |
843 | return array(); |
844 | } |
845 | |
7a6f4066 |
846 | /** |
847 | * Implementation of the function for printing the form elements that control |
848 | * whether the course reset functionality affects the quiz. |
b159da78 |
849 | * @param $course The course id of the course the user is thinking of resetting. |
7a6f4066 |
850 | */ |
851 | function quiz_reset_course_form($course) { |
852 | echo '<p>'; |
853 | print_checkbox('reset_quiz_attempts', 1, true, get_string('removeallquizattempts','quiz')); |
854 | echo '</p>'; |
855 | } |
856 | |
857 | /** |
858 | * Actual implementation of the rest coures functionality, delete all the |
859 | * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is |
860 | * set and true. |
6ef56c99 |
861 | * |
862 | * Also, move the quiz open and close dates, if the course start date is changing. |
863 | * |
7a6f4066 |
864 | * @param $data the data submitted from the reset course forum. |
b159da78 |
865 | * @param $showfeedback whether to output progress information as the reset |
7a6f4066 |
866 | * progresses. |
867 | */ |
868 | function quiz_delete_userdata($data, $showfeedback=true) { |
869 | global $CFG; |
b159da78 |
870 | |
6ef56c99 |
871 | /// Delete attempts. |
872 | if (!empty($data->reset_quiz_attempts)) { |
873 | $conditiononquizids = 'quiz IN (SELECT id FROM ' . |
874 | $CFG->prefix . 'quiz q WHERE q.course = ' . $data->courseid . ')'; |
875 | |
876 | $attemptids = get_records_select('quiz_attempts', $conditiononquizids, '', 'id, uniqueid'); |
877 | if ($attemptids) { |
7a6f4066 |
878 | if ($showfeedback) { |
6ef56c99 |
879 | echo '<div class="notifysuccess">', get_string('deletingquestionattempts', 'quiz'); |
880 | $divider = ': '; |
881 | } |
882 | foreach ($attemptids as $attemptid) { |
883 | delete_attempt($attemptid->uniqueid); |
884 | if ($showfeedback) { |
885 | echo $divider, $attemptid->uniqueid; |
886 | $divider = ', '; |
887 | } |
888 | } |
889 | if ($showfeedback) { |
890 | echo "</div><br />\n"; |
7a6f4066 |
891 | } |
892 | } |
6ef56c99 |
893 | if (delete_records_select('quiz_grades', $conditiononquizids) && $showfeedback) { |
894 | notify(get_string('gradesdeleted','quiz'), 'notifysuccess'); |
895 | } |
896 | if (delete_records_select('quiz_attempts', $conditiononquizids) && $showfeedback) { |
897 | notify(get_string('attemptsdeleted','quiz'), 'notifysuccess'); |
7a6f4066 |
898 | } |
899 | } |
6ef56c99 |
900 | |
901 | /// Update open and close dates |
902 | if (!empty($data->reset_start_date)) { |
903 | /// Work out offset. |
904 | $olddate = get_field('course', 'startdate', 'id', $data->courseid); |
905 | $olddate = usergetmidnight($olddate); // time part of $olddate should be zero |
906 | $newdate = make_timestamp($data->startyear, $data->startmonth, $data->startday); |
907 | $interval = $newdate - $olddate; |
908 | |
909 | /// Apply it to quizzes with an open or close date. |
910 | $success = true; |
911 | begin_sql(); |
912 | $success = $success && execute_sql( |
913 | "UPDATE {$CFG->prefix}quiz |
914 | SET timeopen = timeopen + $interval |
915 | WHERE course = {$data->courseid} AND timeopen <> 0", false); |
916 | $success = $success && execute_sql( |
917 | "UPDATE {$CFG->prefix}quiz |
918 | SET timeclose = timeclose + $interval |
919 | WHERE course = {$data->courseid} AND timeclose <> 0", false); |
920 | |
921 | if ($success) { |
922 | commit_sql(); |
923 | if ($showfeedback) { |
924 | notify(get_string('openclosedatesupdated', 'quiz'), 'notifysuccess'); |
925 | } |
926 | } else { |
927 | rollback_sql(); |
928 | } |
7a6f4066 |
929 | } |
930 | } |
14e6dc79 |
931 | |
932 | /** |
933 | * Checks whether the current user is allowed to view a file uploaded in a quiz. |
934 | * Teachers can view any from their courses, students can only view their own. |
b159da78 |
935 | * |
14e6dc79 |
936 | * @param int $attemptid int attempt id |
937 | * @param int $questionid int question id |
b159da78 |
938 | * @return boolean to indicate access granted or denied |
14e6dc79 |
939 | */ |
940 | function quiz_check_file_access($attemptid, $questionid) { |
941 | global $USER; |
b159da78 |
942 | |
14e6dc79 |
943 | $attempt = get_record("quiz_attempts", 'id', $attemptid); |
944 | $quiz = get_record("quiz", 'id', $attempt->quiz); |
945 | $context = get_context_instance(CONTEXT_COURSE, $quiz->course); |
b159da78 |
946 | |
14e6dc79 |
947 | // access granted if the current user submitted this file |
948 | if ($attempt->userid == $USER->id) { |
949 | return true; |
950 | // access granted if the current user has permission to grade quizzes in this course |
951 | } else if (has_capability('mod/quiz:viewreports', $context) || has_capability('mod/quiz:grade', $context)) { |
952 | return true; |
953 | } |
b159da78 |
954 | |
955 | // otherwise, this user does not have permission |
14e6dc79 |
956 | return false; |
957 | } |
f4d24ff5 |
958 | ?> |