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