Use "modified" date of posts when displaying them, not the "created" date.
[moodle.git] / mod / quiz / lib.php
CommitLineData
730fd187 1<?PHP // $Id$
2
a5e1f35c 3/// Library of function for module quiz
730fd187 4
a5e1f35c 5/// CONSTANTS ///////////////////////////////////////////////////////////////////
730fd187 6
a5e1f35c 7define("GRADEHIGHEST", "1");
8define("GRADEAVERAGE", "2");
9define("ATTEMPTFIRST", "3");
10define("ATTEMPTLAST", "4");
11$QUIZ_GRADE_METHOD = array ( GRADEHIGHEST => get_string("gradehighest", "quiz"),
12 GRADEAVERAGE => get_string("gradeaverage", "quiz"),
13 ATTEMPTFIRST => get_string("attemptfirst", "quiz"),
14 ATTEMPTLAST => get_string("attemptlast", "quiz"));
15
54a67a59 16define("SHORTANSWER", "1");
17define("TRUEFALSE", "2");
18define("MULTICHOICE", "3");
19define("RANDOM", "4");
20define("MATCH", "5");
21define("RANDOMSAMATCH", "6");
22
23$QUIZ_QUESTION_TYPE = array ( MULTICHOICE => get_string("multichoice", "quiz"),
24 TRUEFALSE => get_string("truefalse", "quiz"),
25 SHORTANSWER => get_string("shortanswer", "quiz"),
26 MATCH => get_string("match", "quiz"),
27 RANDOMSAMATCH => get_string("randomsamatch", "quiz") );
a5e1f35c 28
49220fa7 29$QUIZ_FILE_FORMAT = array ( "custom" => get_string("custom", "quiz"),
30 "webct" => get_string("webct", "quiz"),
31 "qti" => get_string("qti", "quiz"),
32 "missingword" => get_string("missingword", "quiz") );
33
7d2e5b65 34define("QUIZ_PICTURE_DEFAULT_HEIGHT", "200");
a5e1f35c 35
54a67a59 36define("QUIZ_MAX_NUMBER_ANSWERS", "8");
37
a5e1f35c 38/// FUNCTIONS ///////////////////////////////////////////////////////////////////
730fd187 39
40function quiz_add_instance($quiz) {
a5e1f35c 41/// Given an object containing all the necessary data,
42/// (defined by the form in mod.html) this function
43/// will create a new instance and return the id number
44/// of the new instance.
730fd187 45
49dcdd18 46 $quiz->created = time();
730fd187 47 $quiz->timemodified = time();
49dcdd18 48 $quiz->timeopen = make_timestamp($quiz->openyear, $quiz->openmonth, $quiz->openday,
c04c41c7 49 $quiz->openhour, $quiz->openminute, 0);
49dcdd18 50 $quiz->timeclose = make_timestamp($quiz->closeyear, $quiz->closemonth, $quiz->closeday,
c04c41c7 51 $quiz->closehour, $quiz->closeminute, 0);
730fd187 52
7bd1aa1d 53 if (!$quiz->id = insert_record("quiz", $quiz)) {
54 return false; // some error occurred
55 }
56
10b9291c 57 // The grades for every question in this quiz are stored in an array
7bd1aa1d 58 if ($quiz->grades) {
59 foreach ($quiz->grades as $question => $grade) {
8d94f5a0 60 if ($question and $grade) {
61 unset($questiongrade);
62 $questiongrade->quiz = $quiz->id;
63 $questiongrade->question = $question;
64 $questiongrade->grade = $grade;
65 if (!insert_record("quiz_question_grades", $questiongrade)) {
66 return false;
67 }
7bd1aa1d 68 }
69 }
70 }
730fd187 71
7bd1aa1d 72 return $quiz->id;
730fd187 73}
74
75
76function quiz_update_instance($quiz) {
a5e1f35c 77/// Given an object containing all the necessary data,
78/// (defined by the form in mod.html) this function
79/// will update an existing instance with new data.
730fd187 80
81 $quiz->timemodified = time();
49dcdd18 82 $quiz->timeopen = make_timestamp($quiz->openyear, $quiz->openmonth, $quiz->openday,
c04c41c7 83 $quiz->openhour, $quiz->openminute, 0);
49dcdd18 84 $quiz->timeclose = make_timestamp($quiz->closeyear, $quiz->closemonth, $quiz->closeday,
c04c41c7 85 $quiz->closehour, $quiz->closeminute, 0);
730fd187 86 $quiz->id = $quiz->instance;
87
7bd1aa1d 88 if (!update_record("quiz", $quiz)) {
89 return false; // some error occurred
90 }
730fd187 91
7bd1aa1d 92
10b9291c 93 // The grades for every question in this quiz are stored in an array
7bd1aa1d 94 // Insert or update records as appropriate
95
96 $existing = get_records("quiz_question_grades", "quiz", $quiz->id, "", "question,grade,id");
97
98 if ($quiz->grades) {
99 foreach ($quiz->grades as $question => $grade) {
8d94f5a0 100 if ($question and $grade) {
101 unset($questiongrade);
102 $questiongrade->quiz = $quiz->id;
103 $questiongrade->question = $question;
104 $questiongrade->grade = $grade;
105 if (isset($existing[$question])) {
106 if ($existing[$question]->grade != $grade) {
107 $questiongrade->id = $existing[$question]->id;
108 if (!update_record("quiz_question_grades", $questiongrade)) {
109 return false;
110 }
111 }
112 } else {
113 if (!insert_record("quiz_question_grades", $questiongrade)) {
7bd1aa1d 114 return false;
115 }
116 }
7bd1aa1d 117 }
118 }
119 }
120
121 return true;
730fd187 122}
123
124
125function quiz_delete_instance($id) {
a5e1f35c 126/// Given an ID of an instance of this module,
127/// this function will permanently delete the instance
128/// and any data that depends on it.
730fd187 129
130 if (! $quiz = get_record("quiz", "id", "$id")) {
131 return false;
132 }
133
134 $result = true;
135
10b9291c 136 if ($attempts = get_record("quiz_attempts", "quiz", "$quiz->id")) {
137 foreach ($attempts as $attempt) {
138 if (! delete_records("quiz_responses", "attempt", "$attempt->id")) {
139 $result = false;
140 }
141 }
142 }
143
144 if (! delete_records("quiz_attempts", "quiz", "$quiz->id")) {
145 $result = false;
146 }
147
148 if (! delete_records("quiz_grades", "quiz", "$quiz->id")) {
149 $result = false;
150 }
151
152 if (! delete_records("quiz_question_grades", "quiz", "$quiz->id")) {
153 $result = false;
154 }
730fd187 155
156 if (! delete_records("quiz", "id", "$quiz->id")) {
157 $result = false;
158 }
159
160 return $result;
161}
162
163function quiz_user_outline($course, $user, $mod, $quiz) {
a5e1f35c 164/// Return a small object with summary information about what a
165/// user has done with a given particular instance of this module
166/// Used for user activity reports.
167/// $return->time = the time they did it
168/// $return->info = a short text description
ebc3bd2b 169 if ($grade = get_record("quiz_grades", "userid", $user->id, "quiz", $quiz->id)) {
98092498 170
171 if ($grade->grade) {
172 $result->info = get_string("grade").": $grade->grade";
173 }
174 $result->time = $grade->timemodified;
175 return $result;
176 }
177 return NULL;
730fd187 178
179 return $return;
180}
181
182function quiz_user_complete($course, $user, $mod, $quiz) {
a5e1f35c 183/// Print a detailed representation of what a user has done with
184/// a given particular instance of this module, for user activity reports.
730fd187 185
186 return true;
187}
188
189function quiz_print_recent_activity(&$logs, $isteacher=false) {
a5e1f35c 190/// Given a list of logs, assumed to be those since the last login
191/// this function prints a short list of changes related to this module
192/// If isteacher is true then perhaps additional information is printed.
193/// This function is called from course/lib.php: print_recent_activity()
730fd187 194
195 global $CFG, $COURSE_TEACHER_COLOR;
196
9c9f7d77 197 $content = "";
198
730fd187 199 return $content; // True if anything was printed, otherwise false
200}
201
202function quiz_cron () {
a5e1f35c 203/// Function to be run periodically according to the moodle cron
204/// This function searches for things that need to be done, such
205/// as sending out mail, toggling flags etc ...
730fd187 206
207 global $CFG;
208
209 return true;
210}
211
d0ac6bc2 212function quiz_grades($quizid) {
858deff0 213/// Must return an array of grades, indexed by user, and a max grade.
214
ebc3bd2b 215 $return->grades = get_records_menu("quiz_grades", "quiz", $quizid, "", "userid,grade");
858deff0 216 $return->maxgrade = get_field("quiz", "grade", "id", "$quizid");
217 return $return;
d0ac6bc2 218}
219
730fd187 220
bdc23be0 221/// SQL FUNCTIONS ////////////////////////////////////////////////////////////////////
222
223function quiz_move_questions($category1, $category2) {
224 global $CFG;
225 return execute_sql("UPDATE {$CFG->prefix}quiz_questions
226 SET category = '$category2'
227 WHERE category = '$category1'",
228 false);
229}
230
231function quiz_get_question_grades($quizid, $questionlist) {
232 global $CFG;
233
234 return get_records_sql("SELECT question,grade
235 FROM {$CFG->prefix}quiz_question_grades
236 WHERE quiz = '$quizid'
237 AND question IN ($questionlist)");
238}
239
240function quiz_get_grade_records($quiz) {
241/// Gets all info required to display the table of quiz results
242/// for report.php
243 global $CFG;
244
245 return get_records_sql("SELECT qg.*, u.firstname, u.lastname, u.picture
246 FROM {$CFG->prefix}quiz_grades qg,
247 {$CFG->prefix}user u
248 WHERE qg.quiz = '$quiz->id'
ebc3bd2b 249 AND qg.userid = u.id");
bdc23be0 250}
251
252function quiz_get_answers($question) {
253// Given a question, returns the correct answers and grades
254 global $CFG;
95dbc030 255
a2fe7cc0 256 switch ($question->qtype) {
95dbc030 257 case SHORTANSWER: // Could be multiple answers
bdc23be0 258 return get_records_sql("SELECT a.*, sa.usecase, g.grade
259 FROM {$CFG->prefix}quiz_shortanswer sa,
260 {$CFG->prefix}quiz_answers a,
261 {$CFG->prefix}quiz_question_grades g
262 WHERE sa.question = '$question->id'
263 AND sa.question = a.question
264 AND sa.question = g.question");
265 break;
266
95dbc030 267 case TRUEFALSE: // Should be always two answers
bdc23be0 268 return get_records_sql("SELECT a.*, g.grade
269 FROM {$CFG->prefix}quiz_answers a,
270 {$CFG->prefix}quiz_question_grades g
271 WHERE a.question = '$question->id'
272 AND a.question = g.question");
273 break;
274
95dbc030 275 case MULTICHOICE: // Should be multiple answers
bdc23be0 276 return get_records_sql("SELECT a.*, mc.single, g.grade
277 FROM {$CFG->prefix}quiz_multichoice mc,
278 {$CFG->prefix}quiz_answers a,
279 {$CFG->prefix}quiz_question_grades g
280 WHERE mc.question = '$question->id'
281 AND mc.question = a.question
282 AND mc.question = g.question");
283 break;
284
95dbc030 285 case RANDOM:
bdc23be0 286 return false; // Not done yet
287 break;
288
54a67a59 289 case MATCH:
290 return get_records_sql("SELECT ms.*, g.grade
291 FROM {$CFG->prefix}quiz_match_sub ms,
292 {$CFG->prefix}quiz_question_grades g
293 WHERE ms.question = '$question->id'
294 AND ms.question = g.question");
295 break;
296
297 case RANDOMSAMATCH: // Could be any of many answers, return them all
95dbc030 298 return get_records_sql("SELECT a.*, g.grade
299 FROM {$CFG->prefix}quiz_questions q,
300 {$CFG->prefix}quiz_answers a,
301 {$CFG->prefix}quiz_question_grades g
302 WHERE q.category = '$question->category'
303 AND q.qtype = ".SHORTANSWER."
304 AND q.id = a.question
305 AND g.question = '$question->id'");
306 break;
307
bdc23be0 308 default:
309 return false;
310 }
311}
312
313
ef4145f6 314function quiz_get_attempt_responses($attempt, $quiz) {
bdc23be0 315// Given an attempt object, this function gets all the
316// stored responses and returns them in a format suitable
317// for regrading using quiz_grade_attempt_results()
318 global $CFG;
319
95dbc030 320 if (!$responses = get_records_sql("SELECT q.id, q.qtype, q.category, r.answer
bdc23be0 321 FROM {$CFG->prefix}quiz_responses r,
322 {$CFG->prefix}quiz_questions q
323 WHERE r.attempt = '$attempt->id'
ef4145f6 324 AND q.id = r.question
325 AND q.id IN ($quiz->questions)")) {
bdc23be0 326 notify("Could not find any responses for that attempt!");
327 return false;
328 }
329
330 foreach ($responses as $key => $response) {
331 $responses[$key]->answer = explode(",",$response->answer);
332 }
333
334 return $responses;
335}
336
337
338
730fd187 339//////////////////////////////////////////////////////////////////////////////////////
a5e1f35c 340/// Any other quiz functions go here. Each of them must have a name that
341/// starts with quiz_
730fd187 342
a8a372cc 343function quiz_print_comment($text) {
344 global $THEME;
345
c897eac5 346 echo "<SPAN CLASS=feedbacktext>".text_to_html($text, true, false)."</SPAN>";
a8a372cc 347}
348
8db3eadd 349function quiz_print_correctanswer($text) {
350 global $THEME;
351
352 echo "<P ALIGN=RIGHT><SPAN CLASS=highlight>$text</SPAN></P>";
353}
354
c74a0ca5 355function quiz_print_question_icon($question) {
356// Prints a question icon
cc3b8c75 357
358 global $QUIZ_QUESTION_TYPE;
359
a2fe7cc0 360 echo "<A HREF=\"question.php?id=$question->id\" TITLE=\"".$QUIZ_QUESTION_TYPE[$question->qtype]."\">";
361 switch ($question->qtype) {
c74a0ca5 362 case SHORTANSWER:
cc3b8c75 363 echo "<IMG BORDER=0 HEIGHT=16 WIDTH=16 SRC=\"pix/sa.gif\">";
c74a0ca5 364 break;
365 case TRUEFALSE:
cc3b8c75 366 echo "<IMG BORDER=0 HEIGHT=16 WIDTH=16 SRC=\"pix/tf.gif\">";
c74a0ca5 367 break;
368 case MULTICHOICE:
cc3b8c75 369 echo "<IMG BORDER=0 HEIGHT=16 WIDTH=16 SRC=\"pix/mc.gif\">";
c74a0ca5 370 break;
371 case RANDOM:
cc3b8c75 372 echo "<IMG BORDER=0 HEIGHT=16 WIDTH=16 SRC=\"pix/rs.gif\">";
c74a0ca5 373 break;
54a67a59 374 case MATCH:
375 echo "<IMG BORDER=0 HEIGHT=16 WIDTH=16 SRC=\"pix/ma.gif\">";
376 break;
377 case RANDOMSAMATCH:
95dbc030 378 echo "<IMG BORDER=0 HEIGHT=16 WIDTH=16 SRC=\"pix/rm.gif\">";
379 break;
c74a0ca5 380 }
cc3b8c75 381 echo "</A>\n";
c74a0ca5 382}
383
384function quiz_print_question($number, $questionid, $grade, $courseid,
54a67a59 385 $feedback=NULL, $response=NULL, $actualgrade=NULL, $correct=NULL) {
a5e1f35c 386/// Prints a quiz question, any format
a8a372cc 387
14d8c0b4 388 if (!$question = get_record("quiz_questions", "id", $questionid)) {
389 notify("Error: Question not found!");
390 }
391
a2fe7cc0 392 if (empty($actualgrade)) {
393 $actualgrade = 0;
394 }
395
14d8c0b4 396 $stranswer = get_string("answer", "quiz");
397 $strmarks = get_string("marks", "quiz");
398
399 echo "<TABLE WIDTH=100% CELLSPACING=10><TR><TD NOWRAP WIDTH=100 VALIGN=top>";
a8a372cc 400 echo "<P ALIGN=CENTER><B>$number</B></P>";
19c4f55c 401 if ($feedback or $response) {
a8a372cc 402 echo "<P ALIGN=CENTER><FONT SIZE=1>$strmarks: $actualgrade/$grade</FONT></P>";
403 } else {
404 echo "<P ALIGN=CENTER><FONT SIZE=1>$grade $strmarks</FONT></P>";
405 }
14d8c0b4 406 print_spacer(1,100);
407 echo "</TD><TD VALIGN=TOP>";
408
a2fe7cc0 409 switch ($question->qtype) {
a5e1f35c 410 case SHORTANSWER:
14d8c0b4 411 if (!$options = get_record("quiz_shortanswer", "question", $question->id)) {
412 notify("Error: Missing question options!");
413 }
2a2c9725 414 echo text_to_html($question->questiontext);
14d8c0b4 415 if ($question->image) {
7d2e5b65 416 print_file_picture($question->image, $courseid, QUIZ_PICTURE_DEFAULT_HEIGHT);
14d8c0b4 417 }
a8a372cc 418 if ($response) {
419 $value = "VALUE=\"$response[0]\"";
a2fe7cc0 420 } else {
421 $value = "";
a8a372cc 422 }
423 echo "<P ALIGN=RIGHT>$stranswer: <INPUT TYPE=TEXT NAME=q$question->id SIZE=20 $value></P>";
424 if ($feedback) {
425 quiz_print_comment("<P ALIGN=right>$feedback[0]</P>");
426 }
8db3eadd 427 if ($correct) {
c74a0ca5 428 $correctanswers = implode(", ", $correct);
8db3eadd 429 quiz_print_correctanswer($correctanswers);
430 }
14d8c0b4 431 break;
432
a5e1f35c 433 case TRUEFALSE:
14d8c0b4 434 if (!$options = get_record("quiz_truefalse", "question", $question->id)) {
435 notify("Error: Missing question options!");
436 }
a2fe7cc0 437 if (!$true = get_record("quiz_answers", "id", $options->trueanswer)) {
14d8c0b4 438 notify("Error: Missing question answers!");
439 }
a2fe7cc0 440 if (!$false = get_record("quiz_answers", "id", $options->falseanswer)) {
14d8c0b4 441 notify("Error: Missing question answers!");
442 }
443 if (!$true->answer) {
444 $true->answer = get_string("true", "quiz");
445 }
446 if (!$false->answer) {
447 $false->answer = get_string("false", "quiz");
448 }
2a2c9725 449 echo text_to_html($question->questiontext);
14d8c0b4 450 if ($question->image) {
7d2e5b65 451 print_file_picture($question->image, $courseid, QUIZ_PICTURE_DEFAULT_HEIGHT);
14d8c0b4 452 }
a8a372cc 453
a2fe7cc0 454 $truechecked = "";
455 $falsechecked = "";
456
457 if (!empty($response[$true->id])) {
a8a372cc 458 $truechecked = "CHECKED";
459 $feedbackid = $true->id;
a2fe7cc0 460 } else if (!empty($response[$false->id])) {
a8a372cc 461 $falsechecked = "CHECKED";
462 $feedbackid = $false->id;
463 }
a2fe7cc0 464
465 $truecorrect = "";
466 $falsecorrect = "";
8db3eadd 467 if ($correct) {
a2fe7cc0 468 if (!empty($correct[$true->id])) {
8db3eadd 469 $truecorrect = "CLASS=highlight";
470 }
a2fe7cc0 471 if (!empty($correct[$false->id])) {
8db3eadd 472 $falsecorrect = "CLASS=highlight";
473 }
474 }
475 echo "<TABLE ALIGN=right cellpadding=5><TR><TD align=right>$stranswer:&nbsp;&nbsp;";
476 echo "<TD $truecorrect>";
a8a372cc 477 echo "<INPUT $truechecked TYPE=RADIO NAME=\"q$question->id\" VALUE=\"$true->id\">$true->answer";
8db3eadd 478 echo "</TD><TD $falsecorrect>";
a8a372cc 479 echo "<INPUT $falsechecked TYPE=RADIO NAME=\"q$question->id\" VALUE=\"$false->id\">$false->answer</P>";
8db3eadd 480 echo "</TD></TR></TABLE><BR CLEAR=ALL>";
a8a372cc 481 if ($feedback) {
482 quiz_print_comment("<P ALIGN=right>$feedback[$feedbackid]</P>");
483 }
484
14d8c0b4 485 break;
486
a5e1f35c 487 case MULTICHOICE:
14d8c0b4 488 if (!$options = get_record("quiz_multichoice", "question", $question->id)) {
489 notify("Error: Missing question options!");
490 }
a5e1f35c 491 if (!$answers = get_records_list("quiz_answers", "id", $options->answers)) {
14d8c0b4 492 notify("Error: Missing question answers!");
493 }
2a2c9725 494 echo text_to_html($question->questiontext);
14d8c0b4 495 if ($question->image) {
7d2e5b65 496 print_file_picture($question->image, $courseid, QUIZ_PICTURE_DEFAULT_HEIGHT);
14d8c0b4 497 }
498 echo "<TABLE ALIGN=right>";
499 echo "<TR><TD valign=top>$stranswer:&nbsp;&nbsp;</TD><TD>";
81b635c3 500 echo "<TABLE>";
14d8c0b4 501 $answerids = explode(",", $options->answers);
a8a372cc 502
14d8c0b4 503 foreach ($answerids as $key => $answerid) {
504 $answer = $answers[$answerid];
68fefdbe 505 $qnumchar = chr(ord('a') + $key);
a8a372cc 506
8e6c87cc 507 if (empty($feedback) or empty($response[$answerid])) {
a8a372cc 508 $checked = "";
8e6c87cc 509 } else {
510 $checked = "CHECKED";
a8a372cc 511 }
14d8c0b4 512 echo "<TR><TD valign=top>";
a5e1f35c 513 if ($options->single) {
a8a372cc 514 echo "<INPUT $checked TYPE=RADIO NAME=q$question->id VALUE=\"$answer->id\">";
14d8c0b4 515 } else {
a8a372cc 516 echo "<INPUT $checked TYPE=CHECKBOX NAME=q$question->id"."a$answer->id VALUE=\"$answer->id\">";
14d8c0b4 517 }
518 echo "</TD>";
8e6c87cc 519 if (empty($feedback) or empty($correct[$answer->id])) {
68fefdbe 520 echo "<TD valign=top>$qnumchar. $answer->answer</TD>";
8e6c87cc 521 } else {
68fefdbe 522 echo "<TD valign=top CLASS=highlight>$qnumchar. $answer->answer</TD>";
8db3eadd 523 }
8e6c87cc 524 if (!empty($feedback)) {
a8a372cc 525 echo "<TD valign=top>&nbsp;";
8e6c87cc 526 if (!empty($response[$answerid])) {
a8a372cc 527 quiz_print_comment($feedback[$answerid]);
528 }
529 echo "</TD>";
530 }
14d8c0b4 531 echo "</TR>";
532 }
533 echo "</TABLE>";
534 echo "</TABLE>";
535 break;
536
8db3eadd 537 case RANDOM:
538 echo "<P>Random questions not supported yet</P>";
539 break;
540
54a67a59 541 case MATCH:
542 if (!$options = get_record("quiz_match", "question", $question->id)) {
543 notify("Error: Missing question options!");
544 }
545 if (!$subquestions = get_records_list("quiz_match_sub", "id", $options->subquestions)) {
546 notify("Error: Missing subquestions for this question!");
547 }
548 echo text_to_html($question->questiontext);
549 if ($question->image) {
550 print_file_picture($question->image, $courseid, QUIZ_PICTURE_DEFAULT_HEIGHT);
551 }
552
553 foreach ($subquestions as $subquestion) {
554 $answers[$subquestion->id] = $subquestion->answertext;
555 }
556
557 $answers = draw_rand_array($answers, count($answers));
558
559 echo "<table border=0 cellpadding=10 align=right>";
560 foreach ($subquestions as $key => $subquestion) {
561 echo "<tr><td align=left valign=top>";
562 echo $subquestion->questiontext;
563 echo "</td>";
564 if (empty($response)) {
565 echo "<td align=right valign=top>";
566 choose_from_menu($answers, "q$question->id"."r$subquestion->id");
567 } else {
568 if (empty($response[$key])) {
569 echo "<td align=right valign=top>";
570 choose_from_menu($answers, "q$question->id"."r$subquestion->id");
571 } else {
572 if ($response[$key] == $correct[$key]) {
573 echo "<td align=right valign=top class=highlight>";
574 choose_from_menu($answers, "q$question->id"."r$subquestion->id", $response[$key]);
575 } else {
576 echo "<td align=right valign=top>";
577 choose_from_menu($answers, "q$question->id"."r$subquestion->id", $response[$key]);
578 }
579 }
580
581 if (!empty($feedback[$key])) {
582 quiz_print_comment($feedback[$key]);
583 }
584 }
585 echo "</td></tr>";
586 }
587 echo "</table>";
588
589 break;
590
591 case RANDOMSAMATCH:
592 if (!$options = get_record("quiz_randomsamatch", "question", $question->id)) {
95dbc030 593 notify("Error: Missing question options!");
594 }
595 echo text_to_html($question->questiontext);
596 if ($question->image) {
597 print_file_picture($question->image, $courseid, QUIZ_PICTURE_DEFAULT_HEIGHT);
598 }
599
600 /// First, get all the questions available
601
602 $allquestions = get_records_select("quiz_questions",
603 "category = $question->category AND qtype = ".SHORTANSWER);
604 if (count($allquestions) < $options->choose) {
605 notify("Error: could not find enough Short Answer questions in the database!");
606 notify("Found ".count($allquestions).", need $options->choose.");
607 break;
608 }
609
610 if (empty($response)) { // Randomly pick the questions
611 if (!$randomquestions = draw_rand_array($allquestions, $options->choose)) {
612 notify("Error choosing $options->choose random questions");
613 break;
614 }
615 } else { // Use existing questions
616 $randomquestions = array();
617 foreach ($response as $key => $rrr) {
618 $rrr = explode("-", $rrr);
619 $randomquestions[$key] = $allquestions[$key];
620 $responseanswer[$key] = $rrr[1];
621 }
622 }
623
624 /// For each selected, find the best matching answers
625
626 foreach ($randomquestions as $randomquestion) {
627 $shortanswerquestion = get_record("quiz_shortanswer", "question", $randomquestion->id);
628 $questionanswers = get_records_list("quiz_answers", "id", $shortanswerquestion->answers);
629 $bestfraction = 0;
630 $bestanswer = NULL;
631 foreach ($questionanswers as $questionanswer) {
632 if ($questionanswer->fraction > $bestfraction) {
633 $bestanswer = $questionanswer;
634 }
635 }
636 if (empty($bestanswer)) {
637 notify("Error: Could not find the best answer for question: ".$randomquestions->name);
638 break;
639 }
640 $randomanswers[$bestanswer->id] = trim($bestanswer->answer);
641 }
642
643 if (!$randomanswers = draw_rand_array($randomanswers, $options->choose)) { // Mix them up
644 notify("Error randomising answers!");
645 break;
646 }
647
648 echo "<table border=0 cellpadding=10>";
649 foreach ($randomquestions as $key => $randomquestion) {
650 echo "<tr><td align=left valign=top>";
651 echo $randomquestion->questiontext;
652 echo "</td>";
653 echo "<td align=right valign=top>";
654 if (empty($response)) {
655 choose_from_menu($randomanswers, "q$question->id"."r$randomquestion->id");
656 } else {
657 if (!empty($correct[$key])) {
658 if ($randomanswers[$responseanswer[$key]] == $correct[$key]) {
659 echo "<span=highlight>";
660 choose_from_menu($randomanswers, "q$question->id"."r$randomquestion->id", $responseanswer[$key]);
661 echo "</span><br \>";
662 } else {
663 choose_from_menu($randomanswers, "q$question->id"."r$randomquestion->id", $responseanswer[$key]);
664 quiz_print_correctanswer($correct[$key]);
665 }
666 } else {
667 choose_from_menu($randomanswers, "q$question->id"."r$randomquestion->id", $responseanswer[$key]);
668 }
669 if (!empty($feedback[$key])) {
670 quiz_print_comment($feedback[$key]);
671 }
672 }
673 echo "</td></tr>";
674 }
675 echo "</table>";
676 break;
677
8db3eadd 678
14d8c0b4 679 default:
680 notify("Error: Unknown question type!");
681 }
682
683 echo "</TD></TR></TABLE>";
3a506ca2 684}
685
a5e1f35c 686function quiz_print_quiz_questions($quiz, $results=NULL) {
687// Prints a whole quiz on one page.
688
8e6c87cc 689 if (empty($quiz->questions)) {
10b9291c 690 notify("No questions have been defined!", "view.php?id=$cm->id");
691 return false;
a5e1f35c 692 }
693
694 $questions = explode(",", $quiz->questions);
695
696 if (!$grades = get_records_list("quiz_question_grades", "question", $quiz->questions, "", "question,grade")) {
10b9291c 697 notify("No grades were found for these questions!");
698 return false;
a5e1f35c 699 }
700
9307692f 701 $strconfirmattempt = addslashes(get_string("readytosend", "quiz"));
702
703 echo "<FORM METHOD=POST ACTION=attempt.php onsubmit=\"return confirm('$strconfirmattempt');\">";
a5e1f35c 704 echo "<INPUT TYPE=hidden NAME=q VALUE=\"$quiz->id\">";
a8a372cc 705
a5e1f35c 706 foreach ($questions as $key => $questionid) {
8db3eadd 707 $feedback = NULL;
708 $response = NULL;
709 $actualgrades = NULL;
710 $correct = NULL;
8e6c87cc 711 if (!empty($results)) {
712 if (!empty($results->feedback[$questionid])) {
713 $feedback = $results->feedback[$questionid];
714 }
715 if (!empty($results->response[$questionid])) {
716 $response = $results->response[$questionid];
717 }
718 if (!empty($results->grades[$questionid])) {
719 $actualgrades = $results->grades[$questionid];
720 }
8db3eadd 721 if ($quiz->correctanswers) {
8e6c87cc 722 if (!empty($results->correct[$questionid])) {
723 $correct = $results->correct[$questionid];
724 }
8db3eadd 725 }
a8a372cc 726 }
8db3eadd 727
a5e1f35c 728 print_simple_box_start("CENTER", "90%");
a8a372cc 729 quiz_print_question($key+1, $questionid, $grades[$questionid]->grade, $quiz->course,
8db3eadd 730 $feedback, $response, $actualgrades, $correct);
a5e1f35c 731 print_simple_box_end();
732 echo "<BR>";
733 }
a8a372cc 734
8e6c87cc 735 if (empty($results)) {
a8a372cc 736 echo "<CENTER><INPUT TYPE=submit VALUE=\"".get_string("savemyanswers", "quiz")."\"></CENTER>";
737 }
a5e1f35c 738 echo "</FORM>";
10b9291c 739
740 return true;
a5e1f35c 741}
6a952ce7 742
743function quiz_get_default_category($courseid) {
744 if ($categories = get_records("quiz_categories", "course", $courseid, "id")) {
745 foreach ($categories as $category) {
746 return $category; // Return the first one (lowest id)
747 }
748 }
749
750 // Otherwise, we need to make one
10b9291c 751 $category->name = get_string("default", "quiz");
752 $category->info = get_string("defaultinfo", "quiz");
6a952ce7 753 $category->course = $courseid;
754 $category->publish = 0;
755
756 if (!$category->id = insert_record("quiz_categories", $category)) {
757 notify("Error creating a default category!");
758 return false;
759 }
760 return $category;
761}
762
c74a0ca5 763function quiz_get_category_menu($courseid, $published=false) {
764 if ($published) {
765 $publish = "OR publish = '1'";
766 }
bdc23be0 767 return get_records_select_menu("quiz_categories", "course='$courseid' $publish", "name ASC", "id,name");
c74a0ca5 768}
769
6a952ce7 770function quiz_print_category_form($course, $current) {
771// Prints a form to choose categories
772
bdc23be0 773 if (!$categories = get_records_select("quiz_categories", "course='$course->id' OR publish = '1'", "name ASC")) {
6a952ce7 774 if (!$category = quiz_get_default_category($course->id)) {
775 notify("Error creating a default category!");
776 return false;
777 }
cb62c00a 778 $categories[$category->id] = $category;
6a952ce7 779 }
8d94f5a0 780 foreach ($categories as $key => $category) {
781 if ($category->publish) {
b55a466b 782 if ($catcourse = get_record("course", "id", $category->course)) {
783 $category->name .= " ($catcourse->shortname)";
8d94f5a0 784 }
785 }
b55a466b 786 $catmenu[$category->id] = $category->name;
8d94f5a0 787 }
6a952ce7 788 $strcategory = get_string("category", "quiz");
789 $strshow = get_string("show", "quiz");
6b069ece 790 $streditcats = get_string("editcategories", "quiz");
6a952ce7 791
467aaec6 792 echo "<TABLE width=\"100%\"><TR><TD NOWRAP>";
6a952ce7 793 echo "<FORM METHOD=POST ACTION=edit.php>";
794 echo "<B>$strcategory:</B>&nbsp;";
b55a466b 795 choose_from_menu($catmenu, "cat", "$current");
92a3c884 796 echo "<INPUT TYPE=submit VALUE=\"$strshow\">";
6a952ce7 797 echo "</FORM>";
6b069ece 798 echo "</TD><TD align=right>";
799 echo "<FORM METHOD=GET ACTION=category.php>";
800 echo "<INPUT TYPE=hidden NAME=id VALUE=\"$course->id\">";
801 echo "<INPUT TYPE=submit VALUE=\"$streditcats\">";
802 echo "</FORM>";
803 echo "</TD></TR></TABLE>";
6a952ce7 804}
805
806
7bd1aa1d 807function quiz_get_all_question_grades($questionlist, $quizid) {
808// Given a list of question IDs, finds grades or invents them to
809// create an array of matching grades
810
bdc23be0 811 $questions = quiz_get_question_grades($quizid, $questionlist);
7bd1aa1d 812
813 $list = explode(",", $questionlist);
814 $grades = array();
815
816 foreach ($list as $qid) {
817 if (isset($questions[$qid])) {
818 $grades[$qid] = $questions[$qid]->grade;
819 } else {
820 $grades[$qid] = 1;
821 }
822 }
823 return $grades;
824}
825
826
827function quiz_print_question_list($questionlist, $grades) {
6a952ce7 828// Prints a list of quiz questions in a small layout form with knobs
7bd1aa1d 829// $questionlist is comma-separated list
830// $grades is an array of corresponding grades
6a952ce7 831
832 global $THEME;
833
834 if (!$questionlist) {
835 echo "<P align=center>";
836 print_string("noquestions", "quiz");
837 echo "</P>";
838 return;
839 }
840
841 $order = explode(",", $questionlist);
842
7bd1aa1d 843 if (!$questions = get_records_list("quiz_questions", "id", $questionlist)) {
6a952ce7 844 error("No questions were found!");
845 }
846
847 $strorder = get_string("order");
848 $strquestionname = get_string("questionname", "quiz");
849 $strgrade = get_string("grade");
850 $strdelete = get_string("delete");
851 $stredit = get_string("edit");
852 $strmoveup = get_string("moveup");
853 $strmovedown = get_string("movedown");
854 $strsavegrades = get_string("savegrades", "quiz");
c74a0ca5 855 $strtype = get_string("type", "quiz");
6a952ce7 856
10b9291c 857 for ($i=10; $i>=0; $i--) {
7bd1aa1d 858 $gradesmenu[$i] = $i;
6a952ce7 859 }
860 $count = 0;
861 $sumgrade = 0;
862 $total = count($order);
863 echo "<FORM METHOD=post ACTION=edit.php>";
864 echo "<TABLE BORDER=0 CELLPADDING=5 CELLSPACING=2 WIDTH=\"100%\">";
467aaec6 865 echo "<TR><TH WIDTH=\"*\" COLSPAN=3 NOWRAP>$strorder</TH><TH align=left WIDTH=\"100%\" NOWRAP>$strquestionname</TH><TH width=\"*\" NOWRAP>$strtype</TH><TH WIDTH=\"*\" NOWRAP>$strgrade</TH><TH WIDTH=\"*\" NOWRAP>$stredit</TH></TR>";
6a952ce7 866 foreach ($order as $qnum) {
95dbc030 867 if (empty($questions[$qnum])) {
868 continue;
869 }
6a952ce7 870 $count++;
871 echo "<TR BGCOLOR=\"$THEME->cellcontent\">";
872 echo "<TD>$count</TD>";
873 echo "<TD>";
874 if ($count != 1) {
875 echo "<A TITLE=\"$strmoveup\" HREF=\"edit.php?up=$qnum\"><IMG
876 SRC=\"../../pix/t/up.gif\" BORDER=0></A>";
877 }
878 echo "</TD>";
879 echo "<TD>";
880 if ($count != $total) {
881 echo "<A TITLE=\"$strmovedown\" HREF=\"edit.php?down=$qnum\"><IMG
882 SRC=\"../../pix/t/down.gif\" BORDER=0></A>";
883 }
884 echo "</TD>";
885 echo "<TD>".$questions[$qnum]->name."</TD>";
467aaec6 886 echo "<TD ALIGN=CENTER>";
c74a0ca5 887 quiz_print_question_icon($questions[$qnum]);
888 echo "</TD>";
6a952ce7 889 echo "<TD>";
8d94f5a0 890 choose_from_menu($gradesmenu, "q$qnum", (string)$grades[$qnum], "");
6a952ce7 891 echo "<TD>";
892 echo "<A TITLE=\"$strdelete\" HREF=\"edit.php?delete=$qnum\"><IMG
893 SRC=\"../../pix/t/delete.gif\" BORDER=0></A>&nbsp;";
894 echo "<A TITLE=\"$stredit\" HREF=\"question.php?id=$qnum\"><IMG
895 SRC=\"../../pix/t/edit.gif\" BORDER=0></A>";
896 echo "</TD>";
897
7bd1aa1d 898 $sumgrade += $grades[$qnum];
6a952ce7 899 }
c74a0ca5 900 echo "<TR><TD COLSPAN=5 ALIGN=right>";
6a952ce7 901 echo "<INPUT TYPE=submit VALUE=\"$strsavegrades:\">";
8d94f5a0 902 echo "<INPUT TYPE=hidden NAME=setgrades VALUE=\"save\">";
6a952ce7 903 echo "<TD ALIGN=LEFT BGCOLOR=\"$THEME->cellcontent\">";
904 echo "<B>$sumgrade</B>";
905 echo "</TD><TD></TD></TR>";
906 echo "</TABLE>";
907 echo "</FORM>";
10b9291c 908
909 return $sumgrade;
6a952ce7 910}
911
912
913function quiz_print_cat_question_list($categoryid) {
914// Prints a form to choose categories
915
916 global $THEME, $QUIZ_QUESTION_TYPE;
917
10b9291c 918 $strcategory = get_string("category", "quiz");
6a952ce7 919 $strquestion = get_string("question", "quiz");
49220fa7 920 $straddquestions = get_string("addquestions", "quiz");
921 $strimportquestions = get_string("importquestions", "quiz");
6a952ce7 922 $strnoquestions = get_string("noquestions", "quiz");
923 $strselect = get_string("select", "quiz");
a01b2571 924 $strselectall = get_string("selectall", "quiz");
6a952ce7 925 $strcreatenewquestion = get_string("createnewquestion", "quiz");
926 $strquestionname = get_string("questionname", "quiz");
927 $strdelete = get_string("delete");
928 $stredit = get_string("edit");
929 $straddselectedtoquiz = get_string("addselectedtoquiz", "quiz");
c74a0ca5 930 $strtype = get_string("type", "quiz");
6a952ce7 931
932 if (!$categoryid) {
933 echo "<P align=center>";
934 print_string("selectcategoryabove", "quiz");
935 echo "</P>";
936 return;
937 }
a5e1f35c 938
6a952ce7 939 if (!$category = get_record("quiz_categories", "id", "$categoryid")) {
940 notify("Category not found!");
941 return;
942 }
8d94f5a0 943 echo "<CENTER>";
10b9291c 944 echo text_to_html($category->info);
6a952ce7 945
49220fa7 946 echo "<TABLE><TR>";
947 echo "<TD valign=top><B>$straddquestions:</B></TD>";
948 echo "<TD valign=top align=right>";
10b9291c 949 echo "<FORM METHOD=GET ACTION=question.php>";
a2fe7cc0 950 choose_from_menu($QUIZ_QUESTION_TYPE, "qtype", "", "");
6a952ce7 951 echo "<INPUT TYPE=hidden NAME=category VALUE=\"$category->id\">";
7d2e5b65 952 echo "<INPUT TYPE=submit VALUE=\"$strcreatenewquestion\">";
cd63d77e 953 helpbutton("questiontypes", $strcreatenewquestion, "quiz");
6a952ce7 954 echo "</FORM>";
49220fa7 955
956 echo "<FORM METHOD=GET ACTION=import.php>";
957 echo "<INPUT TYPE=hidden NAME=category VALUE=\"$category->id\">";
958 echo "<INPUT TYPE=submit VALUE=\"$strimportquestions\">";
959 helpbutton("import", $strimportquestions, "quiz");
960 echo "</FORM>";
961
962 echo "</TR></TABLE>";
963
8d94f5a0 964 echo "</CENTER>";
6a952ce7 965
14bdb238 966 if (!$questions = get_records("quiz_questions", "category", $category->id, "qtype ASC")) {
6a952ce7 967 echo "<P align=center>";
968 print_string("noquestions", "quiz");
969 echo "</P>";
970 return;
971 }
972
10b9291c 973 $canedit = isteacher($category->course);
974
6a952ce7 975 echo "<FORM METHOD=post ACTION=edit.php>";
976 echo "<TABLE BORDER=0 CELLPADDING=5 CELLSPACING=2 WIDTH=\"100%\">";
467aaec6 977 echo "<TR><TH width=\"*\" NOWRAP>$strselect</TH><TH width=\"100%\" align=left NOWRAP>$strquestionname</TH><TH WIDTH=\"*\" NOWRAP>$strtype</TH>";
10b9291c 978 if ($canedit) {
467aaec6 979 echo "<TH width=\"*\" NOWRAP>$stredit</TH>";
10b9291c 980 }
981 echo "</TR>";
6a952ce7 982 foreach ($questions as $question) {
983 echo "<TR BGCOLOR=\"$THEME->cellcontent\">";
984 echo "<TD ALIGN=CENTER>";
985 echo "<INPUT TYPE=CHECKBOX NAME=q$question->id VALUE=\"1\">";
986 echo "</TD>";
987 echo "<TD>".$question->name."</TD>";
467aaec6 988 echo "<TD ALIGN=CENTER>";
c74a0ca5 989 quiz_print_question_icon($question);
990 echo "</TD>";
10b9291c 991 if ($canedit) {
992 echo "<TD>";
e1c91df0 993 echo "<A TITLE=\"$strdelete\" HREF=\"question.php?id=$question->id&delete=$question->id\"><IMG
10b9291c 994 SRC=\"../../pix/t/delete.gif\" BORDER=0></A>&nbsp;";
995 echo "<A TITLE=\"$stredit\" HREF=\"question.php?id=$question->id\"><IMG
996 SRC=\"../../pix/t/edit.gif\" BORDER=0></A>";
997 echo "</TD></TR>";
998 }
999 echo "</TR>";
6a952ce7 1000 }
1001 echo "<TR><TD COLSPAN=3>";
a01b2571 1002 echo "<INPUT TYPE=submit NAME=add VALUE=\"<< $straddselectedtoquiz\">";
1003 //echo "<INPUT TYPE=submit NAME=delete VALUE=\"XX Delete selected\">";
1004 echo "<INPUT type=button onclick=\"checkall()\" value=\"$strselectall\">";
6a952ce7 1005 echo "</TD></TR>";
1006 echo "</TABLE>";
1007 echo "</FORM>";
1008}
a5e1f35c 1009
3a506ca2 1010
958aafe2 1011function quiz_start_attempt($quizid, $userid, $numattempt) {
1012 $attempt->quiz = $quizid;
ebc3bd2b 1013 $attempt->userid = $userid;
958aafe2 1014 $attempt->attempt = $numattempt;
1015 $attempt->timestart = time();
1016 $attempt->timefinish = 0;
1017 $attempt->timemodified = time();
1018
1019 return insert_record("quiz_attempts", $attempt);
1020}
1021
1022function quiz_get_user_attempt_unfinished($quizid, $userid) {
1023// Returns an object containing an unfinished attempt (if there is one)
ebc3bd2b 1024 return get_record("quiz_attempts", "quiz", $quizid, "userid", $userid, "timefinish", 0);
958aafe2 1025}
1026
3a506ca2 1027function quiz_get_user_attempts($quizid, $userid) {
a5e1f35c 1028// Returns a list of all attempts by a user
ebc3bd2b 1029 return get_records_select("quiz_attempts", "quiz = '$quizid' AND userid = '$userid' AND timefinish > 0",
bdc23be0 1030 "attempt ASC");
3a506ca2 1031}
1032
8d94f5a0 1033
1034function quiz_get_user_attempts_string($quiz, $attempts, $bestgrade) {
1035/// Returns a simple little comma-separated list of all attempts,
6d86b5dc 1036/// with each grade linked to the feedback report and with the best grade highlighted
8d94f5a0 1037
1038 $bestgrade = format_float($bestgrade);
1039 foreach ($attempts as $attempt) {
1040 $attemptgrade = format_float(($attempt->sumgrades / $quiz->sumgrades) * $quiz->grade);
1041 if ($attemptgrade == $bestgrade) {
6d86b5dc 1042 $userattempts[] = "<SPAN CLASS=highlight><A HREF=\"report.php?q=$quiz->id&attempt=$attempt->id\">$attemptgrade</A></SPAN>";
8d94f5a0 1043 } else {
6d86b5dc 1044 $userattempts[] = "<A HREF=\"report.php?q=$quiz->id&attempt=$attempt->id\">$attemptgrade</A>";
8d94f5a0 1045 }
1046 }
1047 return implode(",", $userattempts);
1048}
1049
a5e1f35c 1050function quiz_get_best_grade($quizid, $userid) {
1051/// Get the best current grade for a particular user in a quiz
ebc3bd2b 1052 if (!$grade = get_record("quiz_grades", "quiz", $quizid, "userid", $userid)) {
3a506ca2 1053 return 0;
1054 }
1055
2383cadb 1056 return (round($grade->grade,0));
3a506ca2 1057}
1058
e331eb06 1059function quiz_save_best_grade($quiz, $userid) {
a5e1f35c 1060/// Calculates the best grade out of all attempts at a quiz for a user,
1061/// and then saves that grade in the quiz_grades table.
1062
e331eb06 1063 if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) {
e909c8d0 1064 notify("Could not find any user attempts");
a5e1f35c 1065 return false;
1066 }
1067
1068 $bestgrade = quiz_calculate_best_grade($quiz, $attempts);
1069 $bestgrade = (($bestgrade / $quiz->sumgrades) * $quiz->grade);
1070
ebc3bd2b 1071 if ($grade = get_record("quiz_grades", "quiz", $quiz->id, "userid", $userid)) {
1d2603b1 1072 $grade->grade = round($bestgrade, 2);
a5e1f35c 1073 $grade->timemodified = time();
1074 if (!update_record("quiz_grades", $grade)) {
e909c8d0 1075 notify("Could not update best grade");
a5e1f35c 1076 return false;
1077 }
1078 } else {
1079 $grade->quiz = $quiz->id;
ebc3bd2b 1080 $grade->userid = $userid;
38f03e5a 1081 $grade->grade = round($bestgrade, 2);
a5e1f35c 1082 $grade->timemodified = time();
1083 if (!insert_record("quiz_grades", $grade)) {
e909c8d0 1084 notify("Could not insert new best grade");
a5e1f35c 1085 return false;
1086 }
1087 }
1088 return true;
1089}
1090
1091
3a506ca2 1092function quiz_calculate_best_grade($quiz, $attempts) {
a5e1f35c 1093/// Calculate the best grade for a quiz given a number of attempts by a particular user.
3a506ca2 1094
1095 switch ($quiz->grademethod) {
a5e1f35c 1096
1097 case ATTEMPTFIRST:
3a506ca2 1098 foreach ($attempts as $attempt) {
a5e1f35c 1099 return $attempt->sumgrades;
3a506ca2 1100 }
a5e1f35c 1101 break;
1102
1103 case ATTEMPTLAST:
1104 foreach ($attempts as $attempt) {
1105 $final = $attempt->sumgrades;
1106 }
1107 return $final;
3a506ca2 1108
a5e1f35c 1109 case GRADEAVERAGE:
3a506ca2 1110 $sum = 0;
1111 $count = 0;
1112 foreach ($attempts as $attempt) {
a5e1f35c 1113 $sum += $attempt->sumgrades;
3a506ca2 1114 $count++;
1115 }
1116 return (float)$sum/$count;
1117
3a506ca2 1118 default:
a5e1f35c 1119 case GRADEHIGHEST:
1120 $max = 0;
3a506ca2 1121 foreach ($attempts as $attempt) {
a5e1f35c 1122 if ($attempt->sumgrades > $max) {
1123 $max = $attempt->sumgrades;
1124 }
3a506ca2 1125 }
a5e1f35c 1126 return $max;
1127 }
1128}
1129
1130function quiz_save_attempt($quiz, $questions, $result, $attemptnum) {
1131/// Given a quiz, a list of attempted questions and a total grade
1132/// this function saves EVERYTHING so it can be reconstructed later
1133/// if necessary.
1134
1135 global $USER;
1136
958aafe2 1137 // First find the attempt in the database (start of attempt)
1138
1139 if (!$attempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
1140 notify("Trying to save an attempt that was not started!");
1141 return false;
1142 }
1143
1144 if ($attempt->attempt != $attemptnum) { // Double check.
1145 notify("Number of this attempt is different to the unfinished one!");
1146 return false;
1147 }
1148
1149 // Now let's complete this record and save it
a5e1f35c 1150
a5e1f35c 1151 $attempt->sumgrades = $result->sumgrades;
958aafe2 1152 $attempt->timefinish = time();
a5e1f35c 1153 $attempt->timemodified = time();
1154
958aafe2 1155 if (! update_record("quiz_attempts", $attempt)) {
7520988b 1156 notify("Error while saving attempt");
a5e1f35c 1157 return false;
1158 }
1159
1160 // Now let's save all the questions for this attempt
1161
1162 foreach ($questions as $question) {
1163 $response->attempt = $attempt->id;
1164 $response->question = $question->id;
1165 $response->grade = $result->grades[$question->id];
54d0590b 1166 if (!empty($question->answer)) {
a5e1f35c 1167 $response->answer = implode(",",$question->answer);
1168 } else {
1169 $response->answer = "";
1170 }
1171 if (!insert_record("quiz_responses", $response)) {
7520988b 1172 notify("Error while saving response");
a5e1f35c 1173 return false;
1174 }
3a506ca2 1175 }
a5e1f35c 1176 return true;
3a506ca2 1177}
730fd187 1178
a5e1f35c 1179
1180function quiz_grade_attempt_results($quiz, $questions) {
1181/// Given a list of questions (including answers for each one)
1182/// this function does all the hard work of calculating the
1183/// grades for each question, as well as a total grade for
1184/// for the whole quiz. It returns everything in a structure
1185/// that looks like:
1186/// $result->sumgrades (sum of all grades for all questions)
1187/// $result->percentage (Percentage of grades that were correct)
1188/// $result->grade (final grade result for the whole quiz)
1189/// $result->grades[] (array of grades, indexed by question id)
a8a372cc 1190/// $result->response[] (array of response arrays, indexed by question id)
a5e1f35c 1191/// $result->feedback[] (array of feedback arrays, indexed by question id)
8db3eadd 1192/// $result->correct[] (array of feedback arrays, indexed by question id)
a5e1f35c 1193
1194 if (!$questions) {
1195 error("No questions!");
1196 }
1197
1198 $result->sumgrades = 0;
1199
1200 foreach ($questions as $question) {
2a2c9725 1201 if (!$answers = quiz_get_answers($question)) {
1202 error("No answers defined for question id $question->id!");
a5e1f35c 1203 }
1204
1205 $grade = 0; // default
8db3eadd 1206 $correct = array();
19c4f55c 1207 $feedback = array();
1208 $response = array();
a5e1f35c 1209
a2fe7cc0 1210 switch ($question->qtype) {
a5e1f35c 1211 case SHORTANSWER:
1212 if ($question->answer) {
41b95af2 1213 $question->answer = trim(stripslashes($question->answer[0]));
a5e1f35c 1214 } else {
19c4f55c 1215 $question->answer = "";
a5e1f35c 1216 }
a8a372cc 1217 $response[0] = $question->answer;
a2fe7cc0 1218 $bestshortanswer = 0;
a5e1f35c 1219 foreach($answers as $answer) { // There might be multiple right answers
8db3eadd 1220 if ($answer->fraction > $bestshortanswer) {
1221 $correct[$answer->id] = $answer->answer;
a2fe7cc0 1222 $bestshortanswer = $answer->fraction;
8db3eadd 1223 }
2a2c9725 1224 if (!$answer->usecase) { // Don't compare case
a5e1f35c 1225 $answer->answer = strtolower($answer->answer);
1226 $question->answer = strtolower($question->answer);
1227 }
1228 if ($question->answer == $answer->answer) {
a8a372cc 1229 $feedback[0] = $answer->feedback;
a5e1f35c 1230 $grade = (float)$answer->fraction * $answer->grade;
1231 }
1232 }
1233 break;
1234
1235
1236 case TRUEFALSE:
1237 if ($question->answer) {
1238 $question->answer = $question->answer[0];
1239 } else {
1240 $question->answer = NULL;
1241 }
1242 foreach($answers as $answer) { // There should be two answers (true and false)
1243 $feedback[$answer->id] = $answer->feedback;
8db3eadd 1244 if ($answer->fraction > 0) {
1245 $correct[$answer->id] = true;
1246 }
a5e1f35c 1247 if ($question->answer == $answer->id) {
1248 $grade = (float)$answer->fraction * $answer->grade;
a8a372cc 1249 $response[$answer->id] = true;
a5e1f35c 1250 }
1251 }
1252 break;
1253
1254
1255 case MULTICHOICE:
1256 foreach($answers as $answer) { // There will be multiple answers, perhaps more than one is right
1257 $feedback[$answer->id] = $answer->feedback;
8db3eadd 1258 if ($answer->fraction > 0) {
1259 $correct[$answer->id] = true;
1260 }
54d0590b 1261 if (!empty($question->answer)) {
a5e1f35c 1262 foreach ($question->answer as $questionanswer) {
1263 if ($questionanswer == $answer->id) {
95dbc030 1264 $response[$answer->id] = true;
a5e1f35c 1265 if ($answer->single) {
1266 $grade = (float)$answer->fraction * $answer->grade;
1267 continue;
1268 } else {
1269 $grade += (float)$answer->fraction * $answer->grade;
1270 }
1271 }
1272 }
1273 }
1274 }
1275 break;
95dbc030 1276
54a67a59 1277 case MATCH:
1278 $matchcount = $totalcount = 0;
1279
1280 foreach ($question->answer as $questionanswer) { // Each answer is "questionid-answerid"
1281 $totalcount++;
1282 $qarr = explode('-', $questionanswer); // Extract question/answer.
1283 if ($qarr[0] == $qarr[1]) {
1284 $matchcount++;
1285 $correct[$qarr[0]] = true;
1286 $response[$qarr[0]] = $qarr[1];
1287 $questiongrade = $answers[$qarr[0]]->grade;
1288 } else {
1289 $correct[$qarr[0]] = false;
1290 $response[$qarr[0]] = $qarr[1];
1291 }
1292 }
1293
1294 $grade = $questiongrade * $matchcount / $totalcount;
1295
1296 break;
1297
1298 case RANDOMSAMATCH:
95dbc030 1299 $bestanswer = array();
1300 foreach ($answers as $answer) { // Loop through them all looking for correct answers
1301 if (empty($bestanswer[$answer->question])) {
1302 $bestanswer[$answer->question] = 0;
1303 $correct[$answer->question] = "";
1304 }
1305 if ($answer->fraction > $bestanswer[$answer->question]) {
1306 $bestanswer[$answer->question] = $answer->fraction;
1307 $correct[$answer->question] = $answer->answer;
1308 }
1309 }
1310 $answerfraction = 1.0 / (float) count($question->answer);
1311 foreach ($question->answer as $questionanswer) { // For each random answered question
1312 $rqarr = explode('-', $questionanswer); // Extract question/answer.
1313 $rquestion = $rqarr[0];
1314 $ranswer = $rqarr[1];
1315 $response[$rquestion] = $questionanswer;
1316 if (isset($answers[$ranswer])) { // If the answer exists in the list
1317 $answer = $answers[$ranswer];
1318 $feedback[$rquestion] = $answer->feedback;
1319 if ($answer->question == $rquestion) { // Check that this answer matches the question
1320 $grade += (float)$answer->fraction * $answer->grade * $answerfraction;
1321 }
1322 }
1323 }
8db3eadd 1324 break;
a5e1f35c 1325
a5e1f35c 1326 }
1327 if ($grade < 0.0) { // No negative grades
1328 $grade = 0.0;
1329 }
10b9291c 1330
3a50203f 1331 $result->grades[$question->id] = round($grade, 2);
a5e1f35c 1332 $result->sumgrades += $grade;
1333 $result->feedback[$question->id] = $feedback;
a8a372cc 1334 $result->response[$question->id] = $response;
8db3eadd 1335 $result->correct[$question->id] = $correct;
a5e1f35c 1336 }
1337
8d94f5a0 1338 $fraction = (float)($result->sumgrades / $quiz->sumgrades);
1339 $result->percentage = format_float($fraction * 100.0);
1340 $result->grade = format_float($fraction * $quiz->grade);
3a50203f 1341 $result->sumgrades = round($result->sumgrades, 2);
a5e1f35c 1342
1343 return $result;
1344}
6d86b5dc 1345
1346
49220fa7 1347function quiz_save_question_options($question) {
1348/// Given some question info and some data about the the answers
1349/// this function parses, organises and saves the question
1350/// It is used by question.php when saving new data from a
1351/// form, and also by import.php when importing questions
1352///
1353/// Returns $result->error or $result->notice
a5e1f35c 1354
49220fa7 1355 switch ($question->qtype) {
1356 case SHORTANSWER:
1357 // Delete all the old answers
1358 // FIXME - instead of deleting, update existing answers
1359 // so as not to break existing references to them
1360 delete_records("quiz_answers", "question", $question->id);
1361 delete_records("quiz_shortanswer", "question", $question->id);
1362
1363 $answers = array();
1364 $maxfraction = -1;
1365
1366 // Insert all the new answers
1367 foreach ($question->answer as $key => $dataanswer) {
1368 if ($dataanswer != "") {
1369 unset($answer);
1370 $answer->answer = $dataanswer;
1371 $answer->question = $question->id;
1372 $answer->fraction = $question->fraction[$key];
1373 $answer->feedback = $question->feedback[$key];
1374 if (!$answer->id = insert_record("quiz_answers", $answer)) {
1375 $result->error = "Could not insert quiz answer!";
1376 return $result;
1377 }
1378 $answers[] = $answer->id;
1379 if ($question->fraction[$key] > $maxfraction) {
1380 $maxfraction = $question->fraction[$key];
1381 }
1382 }
1383 }
1384
1385 unset($options);
1386 $options->question = $question->id;
1387 $options->answers = implode(",",$answers);
1388 $options->usecase = $question->usecase;
1389 if (!insert_record("quiz_shortanswer", $options)) {
1390 $result->error = "Could not insert quiz shortanswer options!";
1391 return $result;
1392 }
1393
1394 /// Perform sanity checks on fractional grades
1395 if ($maxfraction != 1) {
1396 $maxfraction = $maxfraction * 100;
1397 $result->notice = get_string("fractionsnomax", "quiz", $maxfraction);
1398 return $result;
1399 }
1400 break;
1401 case TRUEFALSE:
1402 // FIXME - instead of deleting, update existing answers
1403 // so as not to break existing references to them
1404 delete_records("quiz_answers", "question", $question->id);
1405 delete_records("quiz_truefalse", "question", $question->id);
1406
1407 $true->answer = get_string("true", "quiz");
1408 $true->question = $question->id;
1409 $true->fraction = $question->answer;
1410 $true->feedback = $question->feedbacktrue;
1411 if (!$true->id = insert_record("quiz_answers", $true)) {
1412 $result->error = "Could not insert quiz answer \"true\")!";
1413 return $result;
1414 }
1415
1416 $false->answer = get_string("false", "quiz");
1417 $false->question = $question->id;
1418 $false->fraction = 1 - (int)$question->answer;
1419 $false->feedback = $question->feedbackfalse;
1420 if (!$false->id = insert_record("quiz_answers", $false)) {
1421 $result->error = "Could not insert quiz answer \"false\")!";
1422 return $result;
1423 }
1424
1425 unset($options);
1426 $options->question = $question->id;
1427 $options->trueanswer = $true->id;
1428 $options->falseanswer = $false->id;
1429 if (!insert_record("quiz_truefalse", $options)) {
1430 $result->error = "Could not insert quiz truefalse options!";
1431 return $result;
1432 }
1433 break;
1434 case MULTICHOICE:
1435 // FIXME - instead of deleting, update existing answers
1436 // so as not to break existing references to them
1437 delete_records("quiz_answers", "question", $question->id);
1438 delete_records("quiz_multichoice", "question", $question->id);
1439
1440 $totalfraction = 0;
1441 $maxfraction = -1;
1442
1443 $answers = array();
1444
1445 // Insert all the new answers
1446 foreach ($question->answer as $key => $dataanswer) {
1447 if ($dataanswer != "") {
1448 unset($answer);
1449 $answer->answer = $dataanswer;
1450 $answer->question = $question->id;
1451 $answer->fraction = $question->fraction[$key];
1452 $answer->feedback = $question->feedback[$key];
1453 if (!$answer->id = insert_record("quiz_answers", $answer)) {
1454 $result->error = "Could not insert quiz answer!";
1455 return $result;
1456 }
1457 $answers[] = $answer->id;
1458
1459 if ($question->fraction[$key] > 0) { // Sanity checks
1460 $totalfraction += $question->fraction[$key];
1461 }
1462 if ($question->fraction[$key] > $maxfraction) {
1463 $maxfraction = $question->fraction[$key];
1464 }
1465 }
1466 }
1467
1468 unset($options);
1469 $options->question = $question->id;
1470 $options->answers = implode(",",$answers);
1471 $options->single = $question->single;
1472 if (!insert_record("quiz_multichoice", $options)) {
1473 $result->error = "Could not insert quiz multichoice options!";
1474 return $result;
1475 }
1476
1477 /// Perform sanity checks on fractional grades
1478 if ($options->single) {
1479 if ($maxfraction != 1) {
1480 $maxfraction = $maxfraction * 100;
1481 $result->notice = get_string("fractionsnomax", "quiz", $maxfraction);
1482 return $result;
1483 }
1484 } else {
1485 $totalfraction = round($totalfraction,2);
1486 if ($totalfraction != 1) {
1487 $totalfraction = $totalfraction * 100;
1488 $result->notice = get_string("fractionsaddwrong", "quiz", $totalfraction);
1489 return $result;
1490 }
1491 }
1492 break;
95dbc030 1493
54a67a59 1494 case MATCH:
1495 delete_records("quiz_match", "question", $question->id);
1496 delete_records("quiz_match_sub", "question", $question->id);
1497
1498 $subquestions = array();
1499
1500 // Insert all the new question+answer pairs
1501 foreach ($question->subquestions as $key => $questiontext) {
1502 $answertext = $question->subanswers[$key];
1503 if (!empty($questiontext) and !empty($answertext)) {
1504 unset($answer);
1505 $subquestion->question = $question->id;
1506 $subquestion->questiontext = $questiontext;
1507 $subquestion->answertext = $answertext;
1508 if (!$subquestion->id = insert_record("quiz_match_sub", $subquestion)) {
1509 $result->error = "Could not insert quiz answer!";
1510 return $result;
1511 }
1512 $subquestions[] = $subquestion->id;
1513 }
1514 }
1515
1516 if (count($subquestions) < 3) {
1517 $result->notice = get_string("notenoughsubquestions", "quiz");
1518 return $result;
1519 }
1520
1521 unset($options);
1522 $options->question = $question->id;
1523 $options->subquestions = implode(",",$subquestions);
1524 if (!insert_record("quiz_match", $options)) {
1525 $result->error = "Could not insert quiz match options!";
1526 return $result;
1527 }
1528
1529 break;
1530
1531 case RANDOMSAMATCH:
95dbc030 1532 $options->question = $question->id;
1533 $options->choose = $question->choose;
54a67a59 1534 if ($existing = get_record("quiz_randomsamatch", "question", $options->question)) {
95dbc030 1535 $options->id = $existing->id;
54a67a59 1536 if (!update_record("quiz_randomsamatch", $options)) {
1537 $result->error = "Could not update quiz randomsamatch options!";
95dbc030 1538 return $result;
1539 }
1540 } else {
54a67a59 1541 if (!insert_record("quiz_randomsamatch", $options)) {
1542 $result->error = "Could not insert quiz randomsamatch options!";
95dbc030 1543 return $result;
1544 }
1545 }
1546 break;
1547
49220fa7 1548 default:
1549 $result->error = "Unsupported question type ($question->qtype)!";
1550 return $result;
1551 break;
1552 }
1553 return true;
1554}
1555
1556
95dbc030 1557
1558
730fd187 1559?>