Teacher can now view student attempts
[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
16define("SHORTANSWER", "1");
17define("TRUEFALSE", "2");
18define("MULTICHOICE", "3");
8db3eadd 19define("RANDOM", "4");
8d94f5a0 20$QUIZ_QUESTION_TYPE = array ( MULTICHOICE => get_string("multichoice", "quiz"),
a5e1f35c 21 TRUEFALSE => get_string("truefalse", "quiz"),
e1c91df0 22 SHORTANSWER => get_string("shortanswer", "quiz") );
a5e1f35c 23
24
25
26/// FUNCTIONS ///////////////////////////////////////////////////////////////////
730fd187 27
28function quiz_add_instance($quiz) {
a5e1f35c 29/// Given an object containing all the necessary data,
30/// (defined by the form in mod.html) this function
31/// will create a new instance and return the id number
32/// of the new instance.
730fd187 33
49dcdd18 34 $quiz->created = time();
730fd187 35 $quiz->timemodified = time();
49dcdd18 36 $quiz->timeopen = make_timestamp($quiz->openyear, $quiz->openmonth, $quiz->openday,
37 $quiz->openhour, $quiz->openminute, $quiz->opensecond);
38 $quiz->timeclose = make_timestamp($quiz->closeyear, $quiz->closemonth, $quiz->closeday,
39 $quiz->closehour, $quiz->closeminute, $quiz->closesecond);
730fd187 40
7bd1aa1d 41 if (!$quiz->id = insert_record("quiz", $quiz)) {
42 return false; // some error occurred
43 }
44
10b9291c 45 // The grades for every question in this quiz are stored in an array
7bd1aa1d 46 if ($quiz->grades) {
47 foreach ($quiz->grades as $question => $grade) {
8d94f5a0 48 if ($question and $grade) {
49 unset($questiongrade);
50 $questiongrade->quiz = $quiz->id;
51 $questiongrade->question = $question;
52 $questiongrade->grade = $grade;
53 if (!insert_record("quiz_question_grades", $questiongrade)) {
54 return false;
55 }
7bd1aa1d 56 }
57 }
58 }
730fd187 59
7bd1aa1d 60 return $quiz->id;
730fd187 61}
62
63
64function quiz_update_instance($quiz) {
a5e1f35c 65/// Given an object containing all the necessary data,
66/// (defined by the form in mod.html) this function
67/// will update an existing instance with new data.
730fd187 68
69 $quiz->timemodified = time();
49dcdd18 70 $quiz->timeopen = make_timestamp($quiz->openyear, $quiz->openmonth, $quiz->openday,
71 $quiz->openhour, $quiz->openminute, $quiz->opensecond);
72 $quiz->timeclose = make_timestamp($quiz->closeyear, $quiz->closemonth, $quiz->closeday,
73 $quiz->closehour, $quiz->closeminute, $quiz->closesecond);
730fd187 74 $quiz->id = $quiz->instance;
75
7bd1aa1d 76 if (!update_record("quiz", $quiz)) {
77 return false; // some error occurred
78 }
730fd187 79
7bd1aa1d 80
10b9291c 81 // The grades for every question in this quiz are stored in an array
7bd1aa1d 82 // Insert or update records as appropriate
83
84 $existing = get_records("quiz_question_grades", "quiz", $quiz->id, "", "question,grade,id");
85
86 if ($quiz->grades) {
87 foreach ($quiz->grades as $question => $grade) {
8d94f5a0 88 if ($question and $grade) {
89 unset($questiongrade);
90 $questiongrade->quiz = $quiz->id;
91 $questiongrade->question = $question;
92 $questiongrade->grade = $grade;
93 if (isset($existing[$question])) {
94 if ($existing[$question]->grade != $grade) {
95 $questiongrade->id = $existing[$question]->id;
96 if (!update_record("quiz_question_grades", $questiongrade)) {
97 return false;
98 }
99 }
100 } else {
101 if (!insert_record("quiz_question_grades", $questiongrade)) {
7bd1aa1d 102 return false;
103 }
104 }
7bd1aa1d 105 }
106 }
107 }
108
109 return true;
730fd187 110}
111
112
113function quiz_delete_instance($id) {
a5e1f35c 114/// Given an ID of an instance of this module,
115/// this function will permanently delete the instance
116/// and any data that depends on it.
730fd187 117
118 if (! $quiz = get_record("quiz", "id", "$id")) {
119 return false;
120 }
121
122 $result = true;
123
10b9291c 124 if ($attempts = get_record("quiz_attempts", "quiz", "$quiz->id")) {
125 foreach ($attempts as $attempt) {
126 if (! delete_records("quiz_responses", "attempt", "$attempt->id")) {
127 $result = false;
128 }
129 }
130 }
131
132 if (! delete_records("quiz_attempts", "quiz", "$quiz->id")) {
133 $result = false;
134 }
135
136 if (! delete_records("quiz_grades", "quiz", "$quiz->id")) {
137 $result = false;
138 }
139
140 if (! delete_records("quiz_question_grades", "quiz", "$quiz->id")) {
141 $result = false;
142 }
730fd187 143
144 if (! delete_records("quiz", "id", "$quiz->id")) {
145 $result = false;
146 }
147
148 return $result;
149}
150
151function quiz_user_outline($course, $user, $mod, $quiz) {
a5e1f35c 152/// Return a small object with summary information about what a
153/// user has done with a given particular instance of this module
154/// Used for user activity reports.
155/// $return->time = the time they did it
156/// $return->info = a short text description
98092498 157 if ($grade = get_record_sql("SELECT * FROM quiz_grades WHERE user = '$user->id' AND quiz = '$quiz->id'")) {
158
159 if ($grade->grade) {
160 $result->info = get_string("grade").": $grade->grade";
161 }
162 $result->time = $grade->timemodified;
163 return $result;
164 }
165 return NULL;
730fd187 166
167 return $return;
168}
169
170function quiz_user_complete($course, $user, $mod, $quiz) {
a5e1f35c 171/// Print a detailed representation of what a user has done with
172/// a given particular instance of this module, for user activity reports.
730fd187 173
174 return true;
175}
176
177function quiz_print_recent_activity(&$logs, $isteacher=false) {
a5e1f35c 178/// Given a list of logs, assumed to be those since the last login
179/// this function prints a short list of changes related to this module
180/// If isteacher is true then perhaps additional information is printed.
181/// This function is called from course/lib.php: print_recent_activity()
730fd187 182
183 global $CFG, $COURSE_TEACHER_COLOR;
184
185 return $content; // True if anything was printed, otherwise false
186}
187
188function quiz_cron () {
a5e1f35c 189/// Function to be run periodically according to the moodle cron
190/// This function searches for things that need to be done, such
191/// as sending out mail, toggling flags etc ...
730fd187 192
193 global $CFG;
194
195 return true;
196}
197
d0ac6bc2 198function quiz_grades($quizid) {
858deff0 199/// Must return an array of grades, indexed by user, and a max grade.
200
201 $return->grades = get_records_sql_menu("SELECT user,grade FROM quiz_grades WHERE quiz = '$quizid'");
202 $return->maxgrade = get_field("quiz", "grade", "id", "$quizid");
203 return $return;
d0ac6bc2 204}
205
730fd187 206
207//////////////////////////////////////////////////////////////////////////////////////
a5e1f35c 208/// Any other quiz functions go here. Each of them must have a name that
209/// starts with quiz_
730fd187 210
a8a372cc 211function quiz_print_comment($text) {
212 global $THEME;
213
214 echo "<FONT COLOR=\"$THEME->cellheading2\">".text_to_html($text, true, false)."</FONT>";
215}
216
8db3eadd 217function quiz_print_correctanswer($text) {
218 global $THEME;
219
220 echo "<P ALIGN=RIGHT><SPAN CLASS=highlight>$text</SPAN></P>";
221}
222
c74a0ca5 223function quiz_print_question_icon($question) {
224// Prints a question icon
225 switch ($question->type) {
226 case SHORTANSWER:
227 echo "<IMG HEIGHT=16 WIDTH=16 SRC=\"pix/sa.gif\">";
228 break;
229 case TRUEFALSE:
230 echo "<IMG HEIGHT=16 WIDTH=16 SRC=\"pix/tf.gif\">";
231 break;
232 case MULTICHOICE:
233 echo "<IMG HEIGHT=16 WIDTH=16 SRC=\"pix/mc.gif\">";
234 break;
235 case RANDOM:
236 echo "<IMG HEIGHT=16 WIDTH=16 SRC=\"pix/rs.gif\">";
237 break;
238 }
239}
240
241function quiz_print_question($number, $questionid, $grade, $courseid,
8db3eadd 242 $feedback=NULL, $response=NULL, $actualgrade=NULL, $correct=NULL) {
a5e1f35c 243/// Prints a quiz question, any format
a8a372cc 244
14d8c0b4 245 if (!$question = get_record("quiz_questions", "id", $questionid)) {
246 notify("Error: Question not found!");
247 }
248
249 $stranswer = get_string("answer", "quiz");
250 $strmarks = get_string("marks", "quiz");
251
252 echo "<TABLE WIDTH=100% CELLSPACING=10><TR><TD NOWRAP WIDTH=100 VALIGN=top>";
a8a372cc 253 echo "<P ALIGN=CENTER><B>$number</B></P>";
19c4f55c 254 if ($feedback or $response) {
a8a372cc 255 echo "<P ALIGN=CENTER><FONT SIZE=1>$strmarks: $actualgrade/$grade</FONT></P>";
256 } else {
257 echo "<P ALIGN=CENTER><FONT SIZE=1>$grade $strmarks</FONT></P>";
258 }
14d8c0b4 259 print_spacer(1,100);
260 echo "</TD><TD VALIGN=TOP>";
261
262 switch ($question->type) {
a5e1f35c 263 case SHORTANSWER:
14d8c0b4 264 if (!$options = get_record("quiz_shortanswer", "question", $question->id)) {
265 notify("Error: Missing question options!");
266 }
2a2c9725 267 echo text_to_html($question->questiontext);
14d8c0b4 268 if ($question->image) {
269 print_file_picture($question->image, $courseid, 200);
270 }
a8a372cc 271 if ($response) {
272 $value = "VALUE=\"$response[0]\"";
273 }
274 echo "<P ALIGN=RIGHT>$stranswer: <INPUT TYPE=TEXT NAME=q$question->id SIZE=20 $value></P>";
275 if ($feedback) {
276 quiz_print_comment("<P ALIGN=right>$feedback[0]</P>");
277 }
8db3eadd 278 if ($correct) {
c74a0ca5 279 $correctanswers = implode(", ", $correct);
8db3eadd 280 quiz_print_correctanswer($correctanswers);
281 }
14d8c0b4 282 break;
283
a5e1f35c 284 case TRUEFALSE:
14d8c0b4 285 if (!$options = get_record("quiz_truefalse", "question", $question->id)) {
286 notify("Error: Missing question options!");
287 }
288 if (!$true = get_record("quiz_answers", "id", $options->true)) {
289 notify("Error: Missing question answers!");
290 }
291 if (!$false = get_record("quiz_answers", "id", $options->false)) {
292 notify("Error: Missing question answers!");
293 }
294 if (!$true->answer) {
295 $true->answer = get_string("true", "quiz");
296 }
297 if (!$false->answer) {
298 $false->answer = get_string("false", "quiz");
299 }
2a2c9725 300 echo text_to_html($question->questiontext);
14d8c0b4 301 if ($question->image) {
302 print_file_picture($question->image, $courseid, 200);
303 }
a8a372cc 304
305 if ($response[$true->id]) {
306 $truechecked = "CHECKED";
307 $feedbackid = $true->id;
308 } else if ($response[$false->id]) {
309 $falsechecked = "CHECKED";
310 $feedbackid = $false->id;
311 }
8db3eadd 312 if ($correct) {
313 if ($correct[$true->id]) {
314 $truecorrect = "CLASS=highlight";
315 }
316 if ($correct[$false->id]) {
317 $falsecorrect = "CLASS=highlight";
318 }
319 }
320 echo "<TABLE ALIGN=right cellpadding=5><TR><TD align=right>$stranswer:&nbsp;&nbsp;";
321 echo "<TD $truecorrect>";
a8a372cc 322 echo "<INPUT $truechecked TYPE=RADIO NAME=\"q$question->id\" VALUE=\"$true->id\">$true->answer";
8db3eadd 323 echo "</TD><TD $falsecorrect>";
a8a372cc 324 echo "<INPUT $falsechecked TYPE=RADIO NAME=\"q$question->id\" VALUE=\"$false->id\">$false->answer</P>";
8db3eadd 325 echo "</TD></TR></TABLE><BR CLEAR=ALL>";
a8a372cc 326 if ($feedback) {
327 quiz_print_comment("<P ALIGN=right>$feedback[$feedbackid]</P>");
328 }
329
14d8c0b4 330 break;
331
a5e1f35c 332 case MULTICHOICE:
14d8c0b4 333 if (!$options = get_record("quiz_multichoice", "question", $question->id)) {
334 notify("Error: Missing question options!");
335 }
a5e1f35c 336 if (!$answers = get_records_list("quiz_answers", "id", $options->answers)) {
14d8c0b4 337 notify("Error: Missing question answers!");
338 }
2a2c9725 339 echo text_to_html($question->questiontext);
14d8c0b4 340 if ($question->image) {
341 print_file_picture($question->image, $courseid, 200);
342 }
343 echo "<TABLE ALIGN=right>";
344 echo "<TR><TD valign=top>$stranswer:&nbsp;&nbsp;</TD><TD>";
345 echo "<TABLE ALIGN=right>";
346 $answerids = explode(",", $options->answers);
a8a372cc 347
14d8c0b4 348 foreach ($answerids as $key => $answerid) {
349 $answer = $answers[$answerid];
350 $qnum = $key + 1;
a8a372cc 351
352 if ($feedback and $response[$answerid]) {
353 $checked = "CHECKED";
354 } else {
355 $checked = "";
356 }
14d8c0b4 357 echo "<TR><TD valign=top>";
a5e1f35c 358 if ($options->single) {
a8a372cc 359 echo "<INPUT $checked TYPE=RADIO NAME=q$question->id VALUE=\"$answer->id\">";
14d8c0b4 360 } else {
a8a372cc 361 echo "<INPUT $checked TYPE=CHECKBOX NAME=q$question->id"."a$answer->id VALUE=\"$answer->id\">";
14d8c0b4 362 }
363 echo "</TD>";
8db3eadd 364 if ($feedback and $correct[$answer->id]) {
365 echo "<TD valign=top CLASS=highlight>$qnum. $answer->answer</TD>";
366 } else {
367 echo "<TD valign=top>$qnum. $answer->answer</TD>";
368 }
a8a372cc 369 if ($feedback) {
370 echo "<TD valign=top>&nbsp;";
371 if ($response[$answerid]) {
372 quiz_print_comment($feedback[$answerid]);
373 }
374 echo "</TD>";
375 }
14d8c0b4 376 echo "</TR>";
377 }
378 echo "</TABLE>";
379 echo "</TABLE>";
380 break;
381
8db3eadd 382 case RANDOM:
383 echo "<P>Random questions not supported yet</P>";
384 break;
385
386
14d8c0b4 387 default:
388 notify("Error: Unknown question type!");
389 }
390
391 echo "</TD></TR></TABLE>";
3a506ca2 392}
393
a5e1f35c 394function quiz_print_quiz_questions($quiz, $results=NULL) {
395// Prints a whole quiz on one page.
396
397 if (!$quiz->questions) {
10b9291c 398 notify("No questions have been defined!", "view.php?id=$cm->id");
399 return false;
a5e1f35c 400 }
401
402 $questions = explode(",", $quiz->questions);
403
404 if (!$grades = get_records_list("quiz_question_grades", "question", $quiz->questions, "", "question,grade")) {
10b9291c 405 notify("No grades were found for these questions!");
406 return false;
a5e1f35c 407 }
408
409 echo "<FORM METHOD=POST ACTION=attempt.php>";
410 echo "<INPUT TYPE=hidden NAME=q VALUE=\"$quiz->id\">";
a8a372cc 411
a5e1f35c 412 foreach ($questions as $key => $questionid) {
8db3eadd 413 $feedback = NULL;
414 $response = NULL;
415 $actualgrades = NULL;
416 $correct = NULL;
a8a372cc 417 if ($results) {
8db3eadd 418 $feedback = $results->feedback[$questionid];
419 $response = $results->response[$questionid];
420 $actualgrades = $results->grades[$questionid];
421 if ($quiz->correctanswers) {
422 $correct = $results->correct[$questionid];
423 }
a8a372cc 424 }
8db3eadd 425
a5e1f35c 426 print_simple_box_start("CENTER", "90%");
a8a372cc 427 quiz_print_question($key+1, $questionid, $grades[$questionid]->grade, $quiz->course,
8db3eadd 428 $feedback, $response, $actualgrades, $correct);
a5e1f35c 429 print_simple_box_end();
430 echo "<BR>";
431 }
a8a372cc 432
433 if (!$results) {
434 echo "<CENTER><INPUT TYPE=submit VALUE=\"".get_string("savemyanswers", "quiz")."\"></CENTER>";
435 }
a5e1f35c 436 echo "</FORM>";
10b9291c 437
438 return true;
a5e1f35c 439}
6a952ce7 440
441function quiz_get_default_category($courseid) {
442 if ($categories = get_records("quiz_categories", "course", $courseid, "id")) {
443 foreach ($categories as $category) {
444 return $category; // Return the first one (lowest id)
445 }
446 }
447
448 // Otherwise, we need to make one
10b9291c 449 $category->name = get_string("default", "quiz");
450 $category->info = get_string("defaultinfo", "quiz");
6a952ce7 451 $category->course = $courseid;
452 $category->publish = 0;
453
454 if (!$category->id = insert_record("quiz_categories", $category)) {
455 notify("Error creating a default category!");
456 return false;
457 }
458 return $category;
459}
460
c74a0ca5 461function quiz_get_category_menu($courseid, $published=false) {
462 if ($published) {
463 $publish = "OR publish = '1'";
464 }
465 return get_records_sql_menu("SELECT id,name FROM quiz_categories WHERE course='$courseid' $publish ORDER by name ASC");
466}
467
6a952ce7 468function quiz_print_category_form($course, $current) {
469// Prints a form to choose categories
470
b55a466b 471 if (!$categories = get_records_sql("SELECT * FROM quiz_categories WHERE course='$course->id' OR publish = '1' ORDER by name ASC")) {
6a952ce7 472 if (!$category = quiz_get_default_category($course->id)) {
473 notify("Error creating a default category!");
474 return false;
475 }
6a952ce7 476 }
8d94f5a0 477 foreach ($categories as $key => $category) {
478 if ($category->publish) {
b55a466b 479 if ($catcourse = get_record("course", "id", $category->course)) {
480 $category->name .= " ($catcourse->shortname)";
8d94f5a0 481 }
482 }
b55a466b 483 $catmenu[$category->id] = $category->name;
8d94f5a0 484 }
6a952ce7 485 $strcategory = get_string("category", "quiz");
486 $strshow = get_string("show", "quiz");
6b069ece 487 $streditcats = get_string("editcategories", "quiz");
6a952ce7 488
6b069ece 489 echo "<TABLE width=\"100%\"><TR><TD>";
6a952ce7 490 echo "<FORM METHOD=POST ACTION=edit.php>";
491 echo "<B>$strcategory:</B>&nbsp;";
b55a466b 492 choose_from_menu($catmenu, "cat", "$current");
92a3c884 493 echo "<INPUT TYPE=submit VALUE=\"$strshow\">";
6a952ce7 494 echo "</FORM>";
6b069ece 495 echo "</TD><TD align=right>";
496 echo "<FORM METHOD=GET ACTION=category.php>";
497 echo "<INPUT TYPE=hidden NAME=id VALUE=\"$course->id\">";
498 echo "<INPUT TYPE=submit VALUE=\"$streditcats\">";
499 echo "</FORM>";
500 echo "</TD></TR></TABLE>";
6a952ce7 501}
502
503
7bd1aa1d 504function quiz_get_all_question_grades($questionlist, $quizid) {
505// Given a list of question IDs, finds grades or invents them to
506// create an array of matching grades
507
92a3c884 508 $questions = get_records_sql("SELECT question,grade FROM quiz_question_grades
7bd1aa1d 509 WHERE quiz = '$quizid'
510 AND question IN ($questionlist)");
511
512 $list = explode(",", $questionlist);
513 $grades = array();
514
515 foreach ($list as $qid) {
516 if (isset($questions[$qid])) {
517 $grades[$qid] = $questions[$qid]->grade;
518 } else {
519 $grades[$qid] = 1;
520 }
521 }
522 return $grades;
523}
524
525
526function quiz_print_question_list($questionlist, $grades) {
6a952ce7 527// Prints a list of quiz questions in a small layout form with knobs
7bd1aa1d 528// $questionlist is comma-separated list
529// $grades is an array of corresponding grades
6a952ce7 530
531 global $THEME;
532
533 if (!$questionlist) {
534 echo "<P align=center>";
535 print_string("noquestions", "quiz");
536 echo "</P>";
537 return;
538 }
539
540 $order = explode(",", $questionlist);
541
7bd1aa1d 542 if (!$questions = get_records_list("quiz_questions", "id", $questionlist)) {
6a952ce7 543 error("No questions were found!");
544 }
545
546 $strorder = get_string("order");
547 $strquestionname = get_string("questionname", "quiz");
548 $strgrade = get_string("grade");
549 $strdelete = get_string("delete");
550 $stredit = get_string("edit");
551 $strmoveup = get_string("moveup");
552 $strmovedown = get_string("movedown");
553 $strsavegrades = get_string("savegrades", "quiz");
c74a0ca5 554 $strtype = get_string("type", "quiz");
6a952ce7 555
10b9291c 556 for ($i=10; $i>=0; $i--) {
7bd1aa1d 557 $gradesmenu[$i] = $i;
6a952ce7 558 }
559 $count = 0;
560 $sumgrade = 0;
561 $total = count($order);
562 echo "<FORM METHOD=post ACTION=edit.php>";
563 echo "<TABLE BORDER=0 CELLPADDING=5 CELLSPACING=2 WIDTH=\"100%\">";
c74a0ca5 564 echo "<TR><TH WIDTH=10 COLSPAN=3>$strorder</TH><TH align=left WIDTH=\"100%\">$strquestionname</TH><TH width=16>$strtype</TH><TH WIDTH=10>$strgrade</TH><TH WIDTH=10>$stredit</TH></TR>";
6a952ce7 565 foreach ($order as $qnum) {
566 $count++;
567 echo "<TR BGCOLOR=\"$THEME->cellcontent\">";
568 echo "<TD>$count</TD>";
569 echo "<TD>";
570 if ($count != 1) {
571 echo "<A TITLE=\"$strmoveup\" HREF=\"edit.php?up=$qnum\"><IMG
572 SRC=\"../../pix/t/up.gif\" BORDER=0></A>";
573 }
574 echo "</TD>";
575 echo "<TD>";
576 if ($count != $total) {
577 echo "<A TITLE=\"$strmovedown\" HREF=\"edit.php?down=$qnum\"><IMG
578 SRC=\"../../pix/t/down.gif\" BORDER=0></A>";
579 }
580 echo "</TD>";
581 echo "<TD>".$questions[$qnum]->name."</TD>";
c74a0ca5 582 echo "<TD WIDTH=16 ALIGN=CENTER>";
583 quiz_print_question_icon($questions[$qnum]);
584 echo "</TD>";
6a952ce7 585 echo "<TD>";
8d94f5a0 586 choose_from_menu($gradesmenu, "q$qnum", (string)$grades[$qnum], "");
6a952ce7 587 echo "<TD>";
588 echo "<A TITLE=\"$strdelete\" HREF=\"edit.php?delete=$qnum\"><IMG
589 SRC=\"../../pix/t/delete.gif\" BORDER=0></A>&nbsp;";
590 echo "<A TITLE=\"$stredit\" HREF=\"question.php?id=$qnum\"><IMG
591 SRC=\"../../pix/t/edit.gif\" BORDER=0></A>";
592 echo "</TD>";
593
7bd1aa1d 594 $sumgrade += $grades[$qnum];
6a952ce7 595 }
c74a0ca5 596 echo "<TR><TD COLSPAN=5 ALIGN=right>";
6a952ce7 597 echo "<INPUT TYPE=submit VALUE=\"$strsavegrades:\">";
8d94f5a0 598 echo "<INPUT TYPE=hidden NAME=setgrades VALUE=\"save\">";
6a952ce7 599 echo "<TD ALIGN=LEFT BGCOLOR=\"$THEME->cellcontent\">";
600 echo "<B>$sumgrade</B>";
601 echo "</TD><TD></TD></TR>";
602 echo "</TABLE>";
603 echo "</FORM>";
10b9291c 604
605 return $sumgrade;
6a952ce7 606}
607
608
609function quiz_print_cat_question_list($categoryid) {
610// Prints a form to choose categories
611
612 global $THEME, $QUIZ_QUESTION_TYPE;
613
10b9291c 614 $strcategory = get_string("category", "quiz");
6a952ce7 615 $strquestion = get_string("question", "quiz");
616 $strnoquestions = get_string("noquestions", "quiz");
617 $strselect = get_string("select", "quiz");
618 $strcreatenewquestion = get_string("createnewquestion", "quiz");
619 $strquestionname = get_string("questionname", "quiz");
620 $strdelete = get_string("delete");
621 $stredit = get_string("edit");
622 $straddselectedtoquiz = get_string("addselectedtoquiz", "quiz");
c74a0ca5 623 $strtype = get_string("type", "quiz");
6a952ce7 624
625 if (!$categoryid) {
626 echo "<P align=center>";
627 print_string("selectcategoryabove", "quiz");
628 echo "</P>";
629 return;
630 }
a5e1f35c 631
6a952ce7 632 if (!$category = get_record("quiz_categories", "id", "$categoryid")) {
633 notify("Category not found!");
634 return;
635 }
8d94f5a0 636 echo "<CENTER>";
10b9291c 637 echo text_to_html($category->info);
6a952ce7 638
10b9291c 639 echo "<FORM METHOD=GET ACTION=question.php>";
6a952ce7 640 echo "<B>$strquestion:</B>&nbsp;";
641 choose_from_menu($QUIZ_QUESTION_TYPE, "type", "", "");
642 echo "<INPUT TYPE=hidden NAME=category VALUE=\"$category->id\">";
643 echo "<INPUT TYPE=submit NAME=new VALUE=\"$strcreatenewquestion\">";
644 echo "</FORM>";
8d94f5a0 645 echo "</CENTER>";
6a952ce7 646
647 if (!$questions = get_records("quiz_questions", "category", $category->id)) {
648 echo "<P align=center>";
649 print_string("noquestions", "quiz");
650 echo "</P>";
651 return;
652 }
653
10b9291c 654 $canedit = isteacher($category->course);
655
6a952ce7 656 echo "<FORM METHOD=post ACTION=edit.php>";
657 echo "<TABLE BORDER=0 CELLPADDING=5 CELLSPACING=2 WIDTH=\"100%\">";
c74a0ca5 658 echo "<TR><TH width=10>$strselect</TH><TH width=* align=left>$strquestionname</TH><TH WIDTH=16>$strtype</TH>";
10b9291c 659 if ($canedit) {
660 echo "<TH width=10>$stredit</TH>";
661 }
662 echo "</TR>";
6a952ce7 663 foreach ($questions as $question) {
664 echo "<TR BGCOLOR=\"$THEME->cellcontent\">";
665 echo "<TD ALIGN=CENTER>";
666 echo "<INPUT TYPE=CHECKBOX NAME=q$question->id VALUE=\"1\">";
667 echo "</TD>";
668 echo "<TD>".$question->name."</TD>";
c74a0ca5 669 echo "<TD WIDTH=16 ALIGN=CENTER>";
670 quiz_print_question_icon($question);
671 echo "</TD>";
10b9291c 672 if ($canedit) {
673 echo "<TD>";
e1c91df0 674 echo "<A TITLE=\"$strdelete\" HREF=\"question.php?id=$question->id&delete=$question->id\"><IMG
10b9291c 675 SRC=\"../../pix/t/delete.gif\" BORDER=0></A>&nbsp;";
676 echo "<A TITLE=\"$stredit\" HREF=\"question.php?id=$question->id\"><IMG
677 SRC=\"../../pix/t/edit.gif\" BORDER=0></A>";
678 echo "</TD></TR>";
679 }
680 echo "</TR>";
6a952ce7 681 }
682 echo "<TR><TD COLSPAN=3>";
683 echo "<INPUT TYPE=hidden NAME=add VALUE=\"1\">";
684 echo "<INPUT TYPE=submit VALUE=\"<< $straddselectedtoquiz\">";
685 echo "</TD></TR>";
686 echo "</TABLE>";
687 echo "</FORM>";
688}
a5e1f35c 689
3a506ca2 690
958aafe2 691function quiz_start_attempt($quizid, $userid, $numattempt) {
692 $attempt->quiz = $quizid;
693 $attempt->user = $userid;
694 $attempt->attempt = $numattempt;
695 $attempt->timestart = time();
696 $attempt->timefinish = 0;
697 $attempt->timemodified = time();
698
699 return insert_record("quiz_attempts", $attempt);
700}
701
702function quiz_get_user_attempt_unfinished($quizid, $userid) {
703// Returns an object containing an unfinished attempt (if there is one)
704 return get_record_sql("SELECT * FROM quiz_attempts
705 WHERE quiz = '$quizid' and user = '$userid' AND timefinish = 0");
706}
707
3a506ca2 708function quiz_get_user_attempts($quizid, $userid) {
a5e1f35c 709// Returns a list of all attempts by a user
958aafe2 710 return get_records_sql("SELECT * FROM quiz_attempts
711 WHERE quiz = '$quizid' and user = '$userid' AND timefinish > 0
712 ORDER by attempt ASC");
3a506ca2 713}
714
8d94f5a0 715
716function quiz_get_user_attempts_string($quiz, $attempts, $bestgrade) {
717/// Returns a simple little comma-separated list of all attempts,
6d86b5dc 718/// with each grade linked to the feedback report and with the best grade highlighted
8d94f5a0 719
720 $bestgrade = format_float($bestgrade);
721 foreach ($attempts as $attempt) {
722 $attemptgrade = format_float(($attempt->sumgrades / $quiz->sumgrades) * $quiz->grade);
723 if ($attemptgrade == $bestgrade) {
6d86b5dc 724 $userattempts[] = "<SPAN CLASS=highlight><A HREF=\"report.php?q=$quiz->id&attempt=$attempt->id\">$attemptgrade</A></SPAN>";
8d94f5a0 725 } else {
6d86b5dc 726 $userattempts[] = "<A HREF=\"report.php?q=$quiz->id&attempt=$attempt->id\">$attemptgrade</A>";
8d94f5a0 727 }
728 }
729 return implode(",", $userattempts);
730}
731
a5e1f35c 732function quiz_get_best_grade($quizid, $userid) {
733/// Get the best current grade for a particular user in a quiz
3a506ca2 734 if (!$grade = get_record_sql("SELECT * FROM quiz_grades WHERE quiz = '$quizid' and user = '$userid'")) {
735 return 0;
736 }
737
2383cadb 738 return (round($grade->grade,0));
3a506ca2 739}
740
579ddad5 741function quiz_get_grade_records($quiz) {
742/// Gets all info required to display the table of quiz results
743/// for report.php
744
745 return get_records_sql("SELECT qg.*, u.firstname, u.lastname, u.picture
746 FROM quiz_grades qg, user u
747 WHERE qg.quiz = '$quiz->id'
748 AND qg.user = u.id");
749}
750
a5e1f35c 751function quiz_save_best_grade($quiz, $user) {
752/// Calculates the best grade out of all attempts at a quiz for a user,
753/// and then saves that grade in the quiz_grades table.
754
755 if (!$attempts = quiz_get_user_attempts($quiz->id, $user->id)) {
e909c8d0 756 notify("Could not find any user attempts");
a5e1f35c 757 return false;
758 }
759
760 $bestgrade = quiz_calculate_best_grade($quiz, $attempts);
761 $bestgrade = (($bestgrade / $quiz->sumgrades) * $quiz->grade);
762
763 if ($grade = get_record_sql("SELECT * FROM quiz_grades WHERE quiz='$quiz->id' AND user='$user->id'")) {
764 $grade->grade = $bestgrade;
765 $grade->timemodified = time();
766 if (!update_record("quiz_grades", $grade)) {
e909c8d0 767 notify("Could not update best grade");
a5e1f35c 768 return false;
769 }
770 } else {
771 $grade->quiz = $quiz->id;
772 $grade->user = $user->id;
38f03e5a 773 $grade->grade = round($bestgrade, 2);
a5e1f35c 774 $grade->timemodified = time();
775 if (!insert_record("quiz_grades", $grade)) {
e909c8d0 776 notify("Could not insert new best grade");
a5e1f35c 777 return false;
778 }
779 }
780 return true;
781}
782
783
2a2c9725 784function quiz_get_answers($question) {
a5e1f35c 785// Given a question, returns the correct answers and grades
786 switch ($question->type) {
787 case SHORTANSWER; // Could be multiple answers
2a2c9725 788 return get_records_sql("SELECT a.*, sa.usecase, g.grade
a5e1f35c 789 FROM quiz_shortanswer sa, quiz_answers a, quiz_question_grades g
790 WHERE sa.question = '$question->id'
791 AND sa.question = a.question
792 AND sa.question = g.question");
793 break;
794
795 case TRUEFALSE; // Should be always two answers
796 return get_records_sql("SELECT a.*, g.grade
797 FROM quiz_answers a, quiz_question_grades g
798 WHERE a.question = '$question->id'
799 AND a.question = g.question");
800 break;
801
802 case MULTICHOICE; // Should be multiple answers
803 return get_records_sql("SELECT a.*, mc.single, g.grade
804 FROM quiz_multichoice mc, quiz_answers a, quiz_question_grades g
805 WHERE mc.question = '$question->id'
806 AND mc.question = a.question
807 AND mc.question = g.question");
808 break;
809
8db3eadd 810 case RANDOM:
811 return false; // Not done yet
812 break;
813
a5e1f35c 814 default:
815 return false;
816 }
817}
818
3a506ca2 819function quiz_calculate_best_grade($quiz, $attempts) {
a5e1f35c 820/// Calculate the best grade for a quiz given a number of attempts by a particular user.
3a506ca2 821
822 switch ($quiz->grademethod) {
a5e1f35c 823
824 case ATTEMPTFIRST:
3a506ca2 825 foreach ($attempts as $attempt) {
a5e1f35c 826 return $attempt->sumgrades;
3a506ca2 827 }
a5e1f35c 828 break;
829
830 case ATTEMPTLAST:
831 foreach ($attempts as $attempt) {
832 $final = $attempt->sumgrades;
833 }
834 return $final;
3a506ca2 835
a5e1f35c 836 case GRADEAVERAGE:
3a506ca2 837 $sum = 0;
838 $count = 0;
839 foreach ($attempts as $attempt) {
a5e1f35c 840 $sum += $attempt->sumgrades;
3a506ca2 841 $count++;
842 }
843 return (float)$sum/$count;
844
3a506ca2 845 default:
a5e1f35c 846 case GRADEHIGHEST:
847 $max = 0;
3a506ca2 848 foreach ($attempts as $attempt) {
a5e1f35c 849 if ($attempt->sumgrades > $max) {
850 $max = $attempt->sumgrades;
851 }
3a506ca2 852 }
a5e1f35c 853 return $max;
854 }
855}
856
857function quiz_save_attempt($quiz, $questions, $result, $attemptnum) {
858/// Given a quiz, a list of attempted questions and a total grade
859/// this function saves EVERYTHING so it can be reconstructed later
860/// if necessary.
861
862 global $USER;
863
958aafe2 864 // First find the attempt in the database (start of attempt)
865
866 if (!$attempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
867 notify("Trying to save an attempt that was not started!");
868 return false;
869 }
870
871 if ($attempt->attempt != $attemptnum) { // Double check.
872 notify("Number of this attempt is different to the unfinished one!");
873 return false;
874 }
875
876 // Now let's complete this record and save it
a5e1f35c 877
a5e1f35c 878 $attempt->sumgrades = $result->sumgrades;
958aafe2 879 $attempt->timefinish = time();
a5e1f35c 880 $attempt->timemodified = time();
881
958aafe2 882 if (! update_record("quiz_attempts", $attempt)) {
7520988b 883 notify("Error while saving attempt");
a5e1f35c 884 return false;
885 }
886
887 // Now let's save all the questions for this attempt
888
889 foreach ($questions as $question) {
890 $response->attempt = $attempt->id;
891 $response->question = $question->id;
892 $response->grade = $result->grades[$question->id];
893 if ($question->answer) {
894 $response->answer = implode(",",$question->answer);
895 } else {
896 $response->answer = "";
897 }
898 if (!insert_record("quiz_responses", $response)) {
7520988b 899 notify("Error while saving response");
a5e1f35c 900 return false;
901 }
3a506ca2 902 }
a5e1f35c 903 return true;
3a506ca2 904}
730fd187 905
a5e1f35c 906
907function quiz_grade_attempt_results($quiz, $questions) {
908/// Given a list of questions (including answers for each one)
909/// this function does all the hard work of calculating the
910/// grades for each question, as well as a total grade for
911/// for the whole quiz. It returns everything in a structure
912/// that looks like:
913/// $result->sumgrades (sum of all grades for all questions)
914/// $result->percentage (Percentage of grades that were correct)
915/// $result->grade (final grade result for the whole quiz)
916/// $result->grades[] (array of grades, indexed by question id)
a8a372cc 917/// $result->response[] (array of response arrays, indexed by question id)
a5e1f35c 918/// $result->feedback[] (array of feedback arrays, indexed by question id)
8db3eadd 919/// $result->correct[] (array of feedback arrays, indexed by question id)
a5e1f35c 920
921 if (!$questions) {
922 error("No questions!");
923 }
924
925 $result->sumgrades = 0;
926
927 foreach ($questions as $question) {
2a2c9725 928 if (!$answers = quiz_get_answers($question)) {
929 error("No answers defined for question id $question->id!");
a5e1f35c 930 }
931
932 $grade = 0; // default
8db3eadd 933 $correct = array();
19c4f55c 934 $feedback = array();
935 $response = array();
a5e1f35c 936
937 switch ($question->type) {
938 case SHORTANSWER:
939 if ($question->answer) {
940 $question->answer = $question->answer[0];
941 } else {
19c4f55c 942 $question->answer = "";
a5e1f35c 943 }
a8a372cc 944 $response[0] = $question->answer;
a5e1f35c 945 foreach($answers as $answer) { // There might be multiple right answers
8db3eadd 946 if ($answer->fraction > $bestshortanswer) {
947 $correct[$answer->id] = $answer->answer;
948 }
2a2c9725 949 if (!$answer->usecase) { // Don't compare case
a5e1f35c 950 $answer->answer = strtolower($answer->answer);
951 $question->answer = strtolower($question->answer);
952 }
953 if ($question->answer == $answer->answer) {
a8a372cc 954 $feedback[0] = $answer->feedback;
a5e1f35c 955 $grade = (float)$answer->fraction * $answer->grade;
956 }
957 }
958 break;
959
960
961 case TRUEFALSE:
962 if ($question->answer) {
963 $question->answer = $question->answer[0];
964 } else {
965 $question->answer = NULL;
966 }
967 foreach($answers as $answer) { // There should be two answers (true and false)
968 $feedback[$answer->id] = $answer->feedback;
8db3eadd 969 if ($answer->fraction > 0) {
970 $correct[$answer->id] = true;
971 }
a5e1f35c 972 if ($question->answer == $answer->id) {
973 $grade = (float)$answer->fraction * $answer->grade;
a8a372cc 974 $response[$answer->id] = true;
a5e1f35c 975 }
976 }
977 break;
978
979
980 case MULTICHOICE:
981 foreach($answers as $answer) { // There will be multiple answers, perhaps more than one is right
982 $feedback[$answer->id] = $answer->feedback;
8db3eadd 983 if ($answer->fraction > 0) {
984 $correct[$answer->id] = true;
985 }
a5e1f35c 986 if ($question->answer) {
987 foreach ($question->answer as $questionanswer) {
988 if ($questionanswer == $answer->id) {
989 if ($answer->single) {
990 $grade = (float)$answer->fraction * $answer->grade;
a8a372cc 991 $response[$answer->id] = true;
a5e1f35c 992 continue;
993 } else {
994 $grade += (float)$answer->fraction * $answer->grade;
a8a372cc 995 $response[$answer->id] = true;
a5e1f35c 996 }
997 }
998 }
999 }
1000 }
1001 break;
8db3eadd 1002 case RANDOM:
1003 // Not done yet
1004 break;
a5e1f35c 1005
1006
1007 }
1008 if ($grade < 0.0) { // No negative grades
1009 $grade = 0.0;
1010 }
10b9291c 1011
a5e1f35c 1012 $result->grades[$question->id] = $grade;
1013 $result->sumgrades += $grade;
1014 $result->feedback[$question->id] = $feedback;
a8a372cc 1015 $result->response[$question->id] = $response;
8db3eadd 1016 $result->correct[$question->id] = $correct;
a5e1f35c 1017 }
1018
8d94f5a0 1019 $fraction = (float)($result->sumgrades / $quiz->sumgrades);
1020 $result->percentage = format_float($fraction * 100.0);
1021 $result->grade = format_float($fraction * $quiz->grade);
a5e1f35c 1022
1023 return $result;
1024}
6d86b5dc 1025
1026
1027function quiz_get_attempt_responses($attempt) {
1028// Given an attempt object, this function gets all the
1029// stored responses and returns them in a format suitable
1030// for regrading using quiz_grade_attempt_results()
1031
1032 if (!$responses = get_records_sql("SELECT q.id, q.type, r.answer
1033 FROM quiz_responses r, quiz_questions q
1034 WHERE r.attempt = '$attempt->id'
1035 AND q.id = r.question")) {
1036 notify("Could not find any responses for that attempt!");
1037 return false;
1038 }
1039
1040 foreach ($responses as $key => $response) {
1041 $responses[$key]->answer = explode(",",$response->answer);
1042 }
1043
1044 return $responses;
1045}
1046
a5e1f35c 1047
730fd187 1048?>