split quiz lib.php into a locallib.php for functions that are only used from within...
[moodle.git] / mod / quiz / locallib.php
CommitLineData
76cacec8 1<?php // $Id$
2
3include_once('lib.php');
4
5/// Library of function for module quiz
6
7/// CONSTANTS ///////////////////////////////////////////////////////////////////
8
9define("GRADEHIGHEST", "1");
10define("GRADEAVERAGE", "2");
11define("ATTEMPTFIRST", "3");
12define("ATTEMPTLAST", "4");
13$QUIZ_GRADE_METHOD = array ( GRADEHIGHEST => get_string("gradehighest", "quiz"),
14 GRADEAVERAGE => get_string("gradeaverage", "quiz"),
15 ATTEMPTFIRST => get_string("attemptfirst", "quiz"),
16 ATTEMPTLAST => get_string("attemptlast", "quiz"));
17
18define("SHORTANSWER", "1");
19define("TRUEFALSE", "2");
20define("MULTICHOICE", "3");
21define("RANDOM", "4");
22define("MATCH", "5");
23define("RANDOMSAMATCH", "6");
24define("DESCRIPTION", "7");
25define("NUMERICAL", "8");
26define("MULTIANSWER", "9");
27define("CALCULATED", "10");
28
29$QUIZ_QUESTION_TYPE = array ( MULTICHOICE => get_string("multichoice", "quiz"),
30 TRUEFALSE => get_string("truefalse", "quiz"),
31 SHORTANSWER => get_string("shortanswer", "quiz"),
32 NUMERICAL => get_string("numerical", "quiz"),
33 CALCULATED => get_string("calculated", "quiz"),
34 MATCH => get_string("match", "quiz"),
35 DESCRIPTION => get_string("description", "quiz"),
36 RANDOMSAMATCH => get_string("randomsamatch", "quiz"),
37 MULTIANSWER => get_string("multianswer", "quiz")
38 );
39
40
41define("QUIZ_PICTURE_MAX_HEIGHT", "600"); // Not currently implemented
42define("QUIZ_PICTURE_MAX_WIDTH", "600"); // Not currently implemented
43
44define("QUIZ_MAX_NUMBER_ANSWERS", "10");
45
46define("QUIZ_CATEGORIES_SORTORDER", "999");
47
48define('QUIZ_REVIEW_AFTER', 1);
49define('QUIZ_REVIEW_BEFORE', 2);
50
51$QUIZ_QTYPES= array();
52
53/// QUIZ_QTYPES INITIATION //////////////////
54class quiz_default_questiontype {
55
56 function name() {
57 return 'default';
58 }
59
60 function uses_quizfile($question, $relativefilepath) {
61 // The default does only check whether the file is used as image:
62 return $question->image == $relativefilepath;
63 }
64
65 function save_question_options($question) {
66 /// Given some question info and some data about the the answers
67 /// this function parses, organises and saves the question
68 /// It is used by question.php through ->save_question when
69 /// saving new data from a form, and also by import.php when
70 /// importing questions
71 ///
72 /// If this is an update, and old answers already exist, then
73 /// these are overwritten using an update(). To do this, it
74 /// it is assumed that the IDs in quiz_answers are in the same
75 /// sort order as the new answers being saved. This should always
76 /// be true, but it's something to keep in mind if fiddling with
77 /// question.php
78 ///
79 /// Returns $result->error or $result->noticeyesno or $result->notice
80
81 /// This default implementation must be overridden:
82
83 $result->error = "Unsupported question type ($question->qtype)!";
84 return $result;
85 }
86
87 function save_question($question, $form, $course) {
88 // This default implementation is suitable for most
89 // question types.
90
91 // First, save the basic question itself
92
93 $question->name = trim($form->name);
94 $question->questiontext = trim($form->questiontext);
95 $question->questiontextformat = $form->questiontextformat;
96
97 if (empty($form->image)) {
98 $question->image = "";
99 } else {
100 $question->image = $form->image;
101 }
102
103 if (empty($question->name)) {
104 $question->name = strip_tags($question->questiontext);
105 if (empty($question->name)) {
106 $question->name = '-';
107 }
108 }
109
110 if (isset($form->defaultgrade)) {
111 $question->defaultgrade = $form->defaultgrade;
112 }
113
114 if (!empty($question->id)) { // Question already exists
115 $question->version ++; // Update version number of question
116 if (!update_record("quiz_questions", $question)) {
117 error("Could not update question!");
118 }
119 } else { // Question is a new one
120 $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed)
121 $question->version = 1;
122 if (!$question->id = insert_record("quiz_questions", $question)) {
123 error("Could not insert new question!");
124 }
125 }
126
127 // Now to save all the answers and type-specific options
128
129 $form->id = $question->id;
130 $form->qtype = $question->qtype;
131 $form->category = $question->category;
132
133 $result = $this->save_question_options($form);
134
135 if (!empty($result->error)) {
136 error($result->error);
137 }
138
139 if (!empty($result->notice)) {
140 notice($result->notice, "question.php?id=$question->id");
141 }
142
143 if (!empty($result->noticeyesno)) {
144 notice_yesno($result->noticeyesno, "question.php?id=$question->id", "edit.php");
145 print_footer($course);
146 exit;
147 }
148
149 redirect("edit.php");
150 }
151
152 /// Convenience function that is used within the question types only
153 function extract_response_id($responsekey) {
154 if (ereg('[0-9]'.$this->name().'([0-9]+)', $responsekey, $regs)) {
155 return $regs[1];
156 } else {
157 return false;
158 }
159 }
160
161
162 function wrapped_questions($question) {
163 /// Overridden only by question types whose questions can
164 /// wrap other questions. Two question types that do this
165 /// are RANDOMSAMATCH and RANDOM
166
167 /// If there are wrapped questions, then this method returns
168 /// comma separated list of them...
169
170 return false;
171 }
172
173 function convert_to_response_answer_field($questionresponse) {
174 /// This function is very much the inverse of extract_response
175 /// This function and extract_response, should be
176 /// obsolete as soon as we get a better response storage
177 /// Right now they are a bridge between a consistent
178 /// response model and the old field answer in quiz_responses
179
180 /// This is the default implemention...
181 return implode(',', $questionresponse);
182 }
183
184 function get_answers($question) {
185 // Returns the answers for the specified question
186
187 // The default behaviour that signals that something is wrong
188 return false;
189 }
190
191 function create_response($question, $nameprefix, $questionsinuse) {
192 /// This rather smart solution works for most cases:
193 $rawresponse->question = $question->id;
194 $rawresponse->answer = '';
195 return $this->extract_response($rawresponse, $nameprefix);
196 }
197
198 function extract_response($rawresponse, $nameprefix) {
199 /// This function is very much the inverse of convert_to_response_answer_field
200 /// This function and convert_to_response_answer_field, should be
201 /// obsolete as soon as we get a better response storage
202 /// Right now they are a bridge between a consistent
203 /// response model and the old field answer in quiz_responses
204
205 /// Default behaviour that works for singlton response question types
206 /// like SHORTANSWER, NUMERICAL and TRUEFALSE
207
208 return array($nameprefix => $rawresponse->answer);
209 }
210
211 function print_question_number_and_grading_details
212 ($number, $grade, $actualgrade=false, $recentlyadded=false, $questionid=0, $courseid=0) {
213
214 /// Print question number and grade:
215
216 global $CFG;
217
218 static $streditquestions, $strmarks, $strrecentlyaddedquestion;
219
220 if (!isset($streditquestions)) {
221 $streditquestions = get_string('editquestions', 'quiz');
222 $strmarks = get_string('marks', 'quiz');
223 $strrecentlyaddedquestion = get_string('recentlyaddedquestion', 'quiz');
224 }
225
226 echo '<center><b>' . $number . '</b>';
227 if ($questionid and isteacher($courseid)) {
228 echo '<br /><font size="1">( ';
229 link_to_popup_window ($CFG->wwwroot.'//mod/quiz/question.php?id='.$questionid,
230 'editquestion', '#'.$questionid, 450, 550, $streditquestions);
231 echo ')</font>';
232 }
233 echo '</center>';
234
235 if (false !== $grade) {
236 //echo '<p align="center"><font size="1">';
237 echo '<br /><center><font size="1">';
238 if (false !== $actualgrade) {
239 echo "$strmarks: $actualgrade/$grade</font></center>";
240 } else {
241 echo "$grade $strmarks</font></center>";
242 }
243 }
244 print_spacer(1,100);
245
246 /// Print possible recently-added information:
247
248 if ($recentlyadded) {
249 echo '</td><td valign="top" align="right">';
250 // Notify the user of this recently added question
251 echo '<font color="red">'.$strrecentlyaddedquestion.'</font>';
252 echo '</td></tr><tr><td></td><td valign="top">';
253
254 } else { // The normal case
255 echo '</td><td valign="top">';
256 }
257 }
258
259 function print_question($currentnumber, $quiz, $question,
260 $readonly, $resultdetails) {
261 /// Note that this method must return the number of the next
262 /// question, making it possible not to increase the number when
263 /// overriding this method (as for qtype=DESCRIPTION).
264
265 echo '<table width="100%" cellspacing="10">';
266 echo '<tr><td nowrap="nowrap" width="100" valign="top">';
267
268 $this->print_question_number_and_grading_details
269 ($currentnumber,
270 $quiz->grade ? $question->maxgrade : false,
271 empty($resultdetails) ? false : $resultdetails->grade,
272 isset($question->recentlyadded) ? $question->recentlyadded : false,
273 $question->id, $quiz->course);
274
275 $this->print_question_formulation_and_controls(
276 $question, $quiz, $readonly,
277 empty($resultdetails) ? false : $resultdetails->answers,
278 empty($resultdetails) ? false : $resultdetails->correctanswers,
279 quiz_qtype_nameprefix($question));
280
281 echo "</td></tr></table>";
282 return $currentnumber + 1;
283 }
284
285 function print_question_formulation_and_controls($question,
286 $quiz, $readonly, $answers, $correctanswers, $nameprefix) {
287 /// This default implementation must be overridden by all
288 /// question type implemenations, unless the default
289 /// implementation of print_question has been overridden...
290
291 notify('Error: Question formulation and input controls has not'
292 .' been implemented for question type '.$this->name());
293 }
294
295 function actual_number_of_questions($question) {
296 /// Used for the feature number-of-questions-per-page
297 /// to determine the actual number of questions wrapped
298 /// by this question. The default is ONE!
299 return 1;
300 }
301
302 function grade_response($question, $nameprefix) {
303 // Analyzes $question->response[] and determines the result
304 // The result is to be returned in this structure:
305 // ->grade (The fraction of maxgrade awarded on the question)
306 // ->answers (result answer records)
307 // ->correctanswers (potential answer records for best ->response[])
308
309 error('grade_response has not been implemented for question type '
310 .$this->name());
311 }
312}
313
314quiz_load_questiontypes();
315function quiz_load_questiontypes() {
316 global $QUIZ_QTYPES;
317 global $CFG;
318
319 $qtypenames= get_list_of_plugins('mod/quiz/questiontypes');
320 foreach($qtypenames as $qtypename) {
321 // Instanciates all plug-in question types
322 $qtypefilepath= "$CFG->dirroot/mod/quiz/questiontypes/$qtypename/questiontype.php";
323
324 // echo "Loading $qtypename<br/>"; // Uncomment for debugging
325 if (is_readable($qtypefilepath)) {
326 require_once($qtypefilepath);
327 }
328 }
329}
330
331
332
333/// SQL FUNCTIONS ////////////////////////////////////////////////////////////////////
334
335function quiz_move_questions($category1, $category2) {
336 global $CFG;
337 return execute_sql("UPDATE {$CFG->prefix}quiz_questions
338 SET category = '$category2'
339 WHERE category = '$category1'",
340 false);
341}
342
343function quiz_get_question_grades($quizid, $questionlist) {
344 global $CFG;
345
346 return get_records_sql("SELECT question,grade
347 FROM {$CFG->prefix}quiz_question_grades
348 WHERE quiz = '$quizid'
349 AND question IN ($questionlist)");
350}
351
352function quiz_get_grade_records($quiz) {
353/// Gets all info required to display the table of quiz results
354/// for report.php
355 global $CFG;
356
357 return get_records_sql("SELECT qg.*, u.firstname, u.lastname, u.picture
358 FROM {$CFG->prefix}quiz_grades qg,
359 {$CFG->prefix}user u
360 WHERE qg.quiz = '$quiz->id'
361 AND qg.userid = u.id");
362}
363
364function quiz_get_answers($question) {
365// Given a question, returns the correct answers for a given question
366 global $QUIZ_QTYPES;
367
368 return $QUIZ_QTYPES[$question->qtype]->get_answers($question);
369}
370
371function quiz_get_attempt_questions($quiz, $attempt, $attempting = false) {
372 /// Returns the questions of the quiz attempt in a format used for
373 /// grading and printing them...
374 ///
375 /// $attempting should be set to true if this function is called in
376 /// order to create an attempt page and false if it is called to create
377 /// a review page.
378 ///
379 /// On top of the ordinary persistent question fields,
380 /// this function also set these properties:
381 //
382 /// ->response - contains names (as keys) and values (as values)
383 /// for all question html-form inputs
384 /// ->recentlyadded - true only if the question has been added to the quiz
385 /// after the responses for the attempt were saved;
386 /// false otherwise
387 /// ->maxgrade - the max grade the question has on the quiz if grades
388 /// are used on the quiz; false otherwise
389
390 global $QUIZ_QTYPES;
391 global $CFG;
392
393 /////////////////////////
394 /// Get the questions:
395 /////////////////////////
396 if (!($questions =
397 get_records_list('quiz_questions', 'id', $quiz->questions))) {
398 notify('Error when reading questions from the database!');
399 return false;
400 }
401
402 ////////////////////////////////////////////
403 /// Determine ->maxgrade for all questions
404 ////////////////////////////////////////////
405 If (!($grades = quiz_get_question_grades($quiz->id, $quiz->questions))) {
406 $grades = array();
407 }
408 foreach ($questions as $qid => $question) {
409 if (isset($grades[$qid])) {
410 $questions[$qid]->maxgrade = $grades[$qid]->grade;
411 } else {
412 $questions[$qid]->maxgrade = 0.0;
413 }
414 }
415
416 //////////////////////////////////////////////////////////////
417 /// Determine attributes ->response and ->recentlyadded (hard)
418 //////////////////////////////////////////////////////////////
419
420 /// Get all existing responses on this attempt
421 $rawresponses = get_records_sql("
422 SELECT question, answer, attempt
423 FROM {$CFG->prefix}quiz_responses
424 WHERE attempt = '$attempt->id' ");
425
426 /// The setting for ->recentlyadded depends on whether this is
427 /// a test attempt or just a review
428 if ($attempting) {
429 /// This is a test attempt so there is a need to create responses
430 /// in case there are none existing.
431 /// Further - the attribute recentlyadded is determined from
432 /// whether the question has a response in the previous attempt,
433 /// which might be used in case the attemptonlast quiz option
434 /// is true.
435
436 $prevattempt = $attempt->attempt;
437 $prevresponses= array();
438 while (--$prevattempt) {
439 $prevresponses = get_records_sql("
440 SELECT r.question, r.answer, r.attempt, r.grade
441 FROM {$CFG->prefix}quiz_responses r, {$CFG->prefix}quiz_attempts a
442 WHERE a.quiz='$quiz->id' AND a.userid='$attempt->userid'
443 AND a.attempt='$prevattempt' AND r.attempt=a.id ");
444 if (!empty($prevresponses)) {
445 break;
446 }
447 }
448
449 $questionsinuse = $quiz->questions; // used if responses must be created
450 foreach ($questions as $qid => $question) {
451 if ($questions[$qid]->recentlyadded =
452 $prevattempt && empty($prevresponses[$qid])) {
453 /* No action */
454
455 } else if ($prevattempt && $quiz->attemptonlast
456 && empty($rawresponses[$qid])) {
457 /// Store the previous response on this attempt!
458 $rawresponses[$qid] = $prevresponses[$qid];
459 $rawresponses[$qid]->attempt = $attempt->id;
460 $rawresponses[$qid]->id =
461 insert_record("quiz_responses", $rawresponses[$qid])
462 or error("Unable to create attemptonlast response for question $qid");
463 }
464
465 /* Extract possible response and its wrapped questions */
466 if (!empty($rawresponses[$qid])) {
467 $questions[$qid]->response = $QUIZ_QTYPES[$question->qtype]
468 ->extract_response($rawresponses[$qid],
469 quiz_qtype_nameprefix($question));
470 /// Catch any additional wrapped questions:
471 if ($wrapped = $QUIZ_QTYPES[$question->qtype]
472 ->wrapped_questions($questions[$question->id],
473 quiz_qtype_nameprefix($question))) {
474 $questionsinuse .= ",$wrapped";
475 }
476 }
477 }
478
479 /// Make sure all the questions will have responses:
480 foreach ($questions as $question) {
481 if (empty($question->response)) {
482 /// No response on this question
483
484 $nameprefix = quiz_qtype_nameprefix($question);
485 $questions[$question->id]->response =
486 $QUIZ_QTYPES[$question->qtype]->create_response
487 ($question, $nameprefix, $questionsinuse);
488
489 //////////////////////////////////////////////
490 // Saving the newly created response before
491 // continuing with the quiz...
492 //////////////////////////////////////////////
493 $responserecord->attempt = $attempt->id;
494 $responserecord->question = $question->id;
495 $responserecord->answer = $QUIZ_QTYPES[$question->qtype]
496 ->convert_to_response_answer_field
497 ($questions[$question->id]->response);
498
499 insert_record("quiz_responses", $responserecord)
500 or error("Unable to create initial response for question $question->id");
501
502 /// Catch any additional wrapped questions:
503 if ($wrapped = $QUIZ_QTYPES[$question->qtype]
504 ->wrapped_questions($questions[$question->id],
505 quiz_qtype_nameprefix($question))) {
506 $questionsinuse .= ",$wrapped";
507 }
508 }
509 }
510
511 } else {
512 /// In the case of review, the recentlyadded flag is set true
513 /// when the question has been added after the attempt and new
514 /// responses are never created
515
516 foreach ($questions as $qid => $question) {
517 if ($questions[$qid]->recentlyadded = empty($rawresponses[$qid])) {
518 /* No action */
519 } else {
520 $questions[$qid]->response = $QUIZ_QTYPES[$question->qtype]
521 ->extract_response($rawresponses[$qid],
522 quiz_qtype_nameprefix($question));
523 }
524 }
525 }
526
527 return $questions;
528}
529
530
531function get_list_of_questions($questionlist) {
532/// Returns an ordered list of questions, including course for each
533
534 global $CFG;
535
536 return get_records_sql("SELECT q.*,c.course
537 FROM {$CFG->prefix}quiz_questions q,
538 {$CFG->prefix}quiz_categories c
539 WHERE q.id in ($questionlist)
540 AND q.category = c.id");
541}
542
543//////////////////////////////////////////////////////////////////////////////////////
544/// Any other quiz functions go here. Each of them must have a name that
545/// starts with quiz_
546
547function quiz_qtype_nameprefix($question, $prefixstart='question') {
548 global $QUIZ_QTYPES;
549 return $prefixstart.$question->id.$QUIZ_QTYPES[$question->qtype]->name();
550}
551function quiz_extract_posted_id($name, $nameprefix='question') {
552 if (ereg("^$nameprefix([0-9]+)", $name, $regs)) {
553 return $regs[1];
554 } else {
555 return false;
556 }
557}
558
559function quiz_print_comment($text) {
560 global $THEME;
561
562 echo "<span class=\"feedbacktext\">".format_text($text, true, false)."</span>";
563}
564
565function quiz_print_correctanswer($text) {
566 global $THEME;
567
568 echo "<p align=\"right\"><span class=\"highlight\">$text</span></p>";
569}
570
571function quiz_print_question_icon($question, $editlink=true) {
572// Prints a question icon
573
574 global $QUIZ_QUESTION_TYPE;
575 global $QUIZ_QTYPES;
576
577 if ($editlink) {
578 echo "<a href=\"question.php?id=$question->id\" title=\""
579 .$QUIZ_QTYPES[$question->qtype]->name()."\">";
580 }
581 echo '<img border="0" height="16" width="16" src="questiontypes/';
582 echo $QUIZ_QTYPES[$question->qtype]->name().'/icon.gif" alt="';
583 echo get_string($QUIZ_QTYPES[$question->qtype]->name(), 'quiz').'" />';
584 if ($editlink) {
585 echo "</a>\n";
586 }
587}
588
589function quiz_print_possible_question_image($quizid, $question) {
590// Includes the question image if there is one
591
592 global $CFG;
593
594 if ($quizid == '') {
595 $quizid = '0';
596 }
597
598 if ($question->image) {
599 echo '<img border="0" src="';
600
601 if (substr(strtolower($question->image), 0, 7) == 'http://') {
602 echo $question->image;
603
604 } else if ($CFG->slasharguments) { // Use this method if possible for better caching
605 echo "$CFG->wwwroot/mod/quiz/quizfile.php/$quizid/$question->id/$question->image";
606
607 } else {
608 echo "$CFG->wwwroot/mod/quiz/quizfile.php?file=/$quizid/$question->id/$question->image";
609 }
610 echo '" alt="" />';
611
612 }
613}
614
615function quiz_navigation_javascript($link) {
616 return "javascript:navigate($link);";
617}
618
619function quiz_print_navigation_panel($questions, $questionsperpage, $navigation) {
620 global $QUIZ_QTYPES;
621
622 $numberinglayout = array();
623 $nextqnumber = 1;
624 foreach ($questions as $question) {
625 if ($qnumberinc = $QUIZ_QTYPES[$question->qtype]
626 ->actual_number_of_questions($question)) {
627 $numberinglayout[] = $nextqnumber;
628 $nextqnumber += $qnumberinc;
629 }
630 }
631
632 if ($nextqnumber - $qnumberinc <= $questionsperpage) {
633 /// The total number of questions does not exceed the maximum
634 /// number of allowed questions per page so...
635 return 0;
636 }
637 /// else - Navigation menu will be printed!
638
639 ///////////////////////////////////////////////
640 /// Determine the layout of the navigation menu
641 ///////////////////////////////////////////////
642 if (1 == $questionsperpage) {
643 /// The simple case:
644 $pagelinkagelayout = $pagenavigationlayout = $numberinglayout;
645
646 } else {
647 /// More complicated:
648 $pagenavigationlayout = array();
649 $pagelinkagelayout = array($currentpagestart = 1);
650 foreach ($numberinglayout as $questionnumber) {
651 if ($questionnumber - $currentpagestart >= $questionsperpage) {
652 $pagenavigationlayout[] = $currentpagestart
653 .'-'. ($questionnumber - 1);
654 if ($currentpagestart < $navigation
655 && $navigation < $questionnumber) {
656 // $navigation is out of sync so adjust for robustness
657 $navigation = $currentpagestart;
658 }
659 $pagelinkagelayout[] = $currentpagestart = $questionnumber;
660 }
661 }
662 $pagenavigationlayout[] = $currentpagestart .'-'. ($nextqnumber - 1);
663 if ($currentpagestart < $navigation) {
664 // $firsquestion is out of sync so adjust it for robustness...
665 $navigation = $currentpagestart;
666 }
667 }
668
669 foreach ($pagelinkagelayout as $key => $link) {
670 if ($link < $navigation) {
671 $previouspagelink = $link;
672 } else if ($link == $navigation) {
673 $currentnavigationtitle = $pagenavigationlayout[$key];
674 } else {
675 $endpagelink = $link;
676 if (false == isset($nextpagelink)) {
677 $nextpagelink = $link;
678 }
679 }
680 }
681
682 ///////////////////////////////////////////////
683 /// Print the navigation meny
684 ///////////////////////////////////////////////
685 print_simple_box_start('center', '*');
686 echo '<table><tr><td colspan="5" align="center"><table><tr>';
687 foreach ($pagelinkagelayout as $key => $link) {
688 echo '<td align="center">&nbsp;';
689 if ($link != $navigation) {
690 echo '<a href="' . quiz_navigation_javascript($link) . '">';
691 }
692 echo $pagenavigationlayout[$key];
693 if ($link != $navigation) {
694 echo '</a>';
695 }
696 echo '&nbsp;</td>';
697 }
698 echo '</tr></table></td></tr><tr><td width="20%" align="left">';
699 if (isset($previouspagelink)) {
700 echo '<a href="' . quiz_navigation_javascript('1') . '">|&lt;&lt;&lt;</a></td><td width="20%" align="center" cellpadding="2">';
701 echo '<a href="' . quiz_navigation_javascript($previouspagelink) . '">&lt;&lt;&lt;</a></td>';
702 } else {
703 echo '</td><td width="20%"></td>';
704 }
705 echo '<td width="20%" align="center"><b>';
706 echo $currentnavigationtitle;
707 echo '</b></td><td width="20%" align="center" cellpadding="2">';
708 if (isset($nextpagelink)) {
709 echo '<a href="';
710 echo quiz_navigation_javascript($nextpagelink);
711 echo '">&gt;&gt;&gt;</a></td><td width="20%" align="right"><a href="';
712 echo quiz_navigation_javascript($endpagelink);
713 echo '">&gt;&gt;&gt;|</a>';
714 } else {
715 echo '</td><td width="20%">';
716 }
717 echo '</td></tr></table>';
718 print_simple_box_end();
719
720 ////////////////////////////////////////////////
721 /// Return the potentially adjusted $navigation
722 ////////////////////////////////////////////////
723 return $navigation;
724}
725
726function quiz_print_quiz_questions($quiz, $questions, $results=NULL,
727 $shuffleorder=NULL, $navigation=0) {
728// Prints a whole quiz on one page.
729
730 if ($navigation < 0) {
731 $navigation = 0; // For robustness
732 }
733
734 global $QUIZ_QTYPES;
735
736 /// Check arguments
737
738 if (empty($questions)) {
739 notify("No questions have been defined!");
740 return false;
741 }
742
743 if (!$shuffleorder) {
744 if (!empty($quiz->shufflequestions)) { // Mix everything up
745 $questions = swapshuffle_assoc($questions);
746 } else {
747 $shuffleorder = explode(",", $quiz->questions); // Use originally defined order
748 }
749 }
750
751 if ($shuffleorder) { // Order has been defined, so reorder questions
752 $oldquestions = $questions;
753 $questions = array();
754 foreach ($shuffleorder as $key) {
755 $questions[] = $oldquestions[$key]; // This loses the index key, but doesn't matter
756 }
757 }
758
759 $strconfirmattempt = addslashes(get_string("readytosend", "quiz"));
760
761 if (empty($quiz->grade)) {
762 $onsubmit = "";
763 } else {
764 $onsubmit = "onsubmit=\"return confirm('$strconfirmattempt');\"";
765 }
766 // BEGIN EDIT
767 if($quiz->timelimit > 0) {
768 ?>
769 <script language="javascript" type="text/javascript">
770 <!--
771 document.write("<form name=\"responseform\" method=\"post\" action=\"attempt.php\" <?php print(addslashes($onsubmit));?>>\n");
772 // -->
773 </script>
774 <noscript>
775 <center><p><strong><?php print_string("noscript","quiz"); ?></strong></p></center>
776 </noscript>
777 <?php
778 } else {
779 echo "<form name=\"responseform\" method=\"post\" action=\"attempt.php\" $onsubmit>\n";
780 }
781 // END EDIT
782 echo "<input type=\"hidden\" name=\"q\" value=\"$quiz->id\" />\n";
783
784 if ($navigation && $quiz->questionsperpage) {
785 echo '<input type="hidden" id="navigation" name="navigation" value="0" />';
786 $navigation = quiz_print_navigation_panel($questions,
787 $quiz->questionsperpage, $navigation);
788 } else {
789 $navigation = 0;
790 }
791
792 $nextquestionnumber = 1;
793 $questionorder = array();
794
795 // $readonly determines if it is an attempt or an review,
796 // The condition used here is unfortunatelly somewhat confusing...
797 $readonly = !empty($results) && !isset($results->attemptbuildsonthelast)
798 ? ' disabled="disabled" ' : '';
799
800 foreach ($questions as $question) {
801
802 if (empty($question->qtype)) { // Just for robustness
803 continue;
804 }
805
806 $questionorder[] = $question->id;
807
808 if (0 == $navigation
809 || $navigation <= $nextquestionnumber
810 && $nextquestionnumber - $navigation < $quiz->questionsperpage) {
811 if ($results && isset($results->details[$question->id])) {
812 $details = $results->details[$question->id];
813 } else {
814 $details = false;
815 }
816
817 echo "<br />";
818 print_simple_box_start("center", "90%");
819 $nextquestionnumber = $QUIZ_QTYPES[$question->qtype]->print_question
820 ($nextquestionnumber, $quiz, $question, $readonly, $details);
821 print_simple_box_end();
822 } else {
823 $nextquestionnumber += $QUIZ_QTYPES[$question->qtype]
824 ->actual_number_of_questions($question);
825 }
826 }
827
828 if ($navigation) {
829 quiz_print_navigation_panel($questions, $quiz->questionsperpage,
830 $navigation);
831 }
832 echo "<br />";
833
834 if (empty($readonly)) {
835 if (!empty($quiz->shufflequestions)) { // Things have been mixed up, so pass the question order
836 $shuffleorder = implode(',', $questionorder);
837 echo "<input type=\"hidden\" name=\"shuffleorder\" value=\"$shuffleorder\" />\n";
838 }
839 if($quiz->timelimit > 0) {
840 echo "<script language=\"javascript\" type=\"text/javascript\">\n";
841 echo "<!--\n";
842 echo "document.write('<center><input type=\"button\" value=\"".get_string("savemyanswers", "quiz")."\" onclick=\"return send_data();\" /></center>');\n";
843 echo "// -->\n";
844 echo "</script>\n";
845 echo "<noscript>\n";
846 echo "<center><strong>".get_string("noscript","quiz")."</strong></center>\n";
847 echo "</noscript>\n";
848 } else {
849 echo "<center>\n<input type=\"submit\" value=\"".get_string("savemyanswers", "quiz")."\" />\n</center>";
850 }
851 }
852 echo "</form>";
853
854 if ($navigation && $quiz->questionsperpage) {
855 echo '<script language="javascript" type="text/javascript">';
856 echo "function navigate(link) {
857 document.responseform.navigation.value=link;
858 document.responseform.submit();
859 }
860 </script>";
861 }
862
863 return true;
864}
865
866
867
868function quiz_get_default_category($courseid) {
869/// Returns the current category
870
871 if ($categories = get_records_select("quiz_categories", "course = '$courseid' AND parent = '0'", "id")) {
872 foreach ($categories as $category) {
873 return $category; // Return the first one (lowest id)
874 }
875 }
876
877 // Otherwise, we need to make one
878 $category->name = get_string("default", "quiz");
879 $category->info = get_string("defaultinfo", "quiz");
880 $category->course = $courseid;
881 $category->parent = 0;
882 $category->sortorder = QUIZ_CATEGORIES_SORTORDER;
883 $category->publish = 0;
884 $category->stamp = make_unique_id_code();
885
886 if (!$category->id = insert_record("quiz_categories", $category)) {
887 notify("Error creating a default category!");
888 return false;
889 }
890 return $category;
891}
892
893function quiz_get_category_menu($courseid, $published=false) {
894/// Returns the list of categories
895 $publish = "";
896 if ($published) {
897 $publish = "OR publish = '1'";
898 }
899
900 if (!isadmin()) {
901 $categories = get_records_select("quiz_categories", "course = '$courseid' $publish", 'parent, sortorder, name ASC');
902 } else {
903 $categories = get_records_select("quiz_categories", '', 'parent, sortorder, name ASC');
904 }
905 if (!$categories) {
906 return false;
907 }
908 $categories = add_indented_names($categories);
909
910 foreach ($categories as $category) {
911 if ($catcourse = get_record("course", "id", $category->course)) {
912 if ($category->publish && ($category->course != $courseid)) {
913 $category->indentedname .= " ($catcourse->shortname)";
914 }
915 $catmenu[$category->id] = $category->indentedname;
916 }
917 }
918 return $catmenu;
919}
920
921function quiz_print_category_form($course, $current, $recurse=1) {
922/// Prints a form to choose categories
923
924/// Make sure the default category exists for this course
925 if (!$categories = get_records("quiz_categories", "course", $course->id, "id ASC")) {
926 if (!$category = quiz_get_default_category($course->id)) {
927 notify("Error creating a default category!");
928 }
929 }
930
931/// Get all the existing categories now
932 if (!$categories = get_records_select("quiz_categories", "course = '{$course->id}' OR publish = '1'", "parent, sortorder, name ASC")) {
933 notify("Could not find any question categories!");
934 return false; // Something is really wrong
935 }
936 $categories = add_indented_names($categories);
937 foreach ($categories as $key => $category) {
938 if ($catcourse = get_record("course", "id", $category->course)) {
939 if ($category->publish && $category->course != $course->id) {
940 $category->indentedname .= " ($catcourse->shortname)";
941 }
942 $catmenu[$category->id] = $category->indentedname;
943 }
944 }
945 $strcategory = get_string("category", "quiz");
946 $strshow = get_string("show", "quiz");
947 $streditcats = get_string("editcategories", "quiz");
948
949 echo "<table width=\"100%\"><tr><td width=\"20\" nowrap=\"nowrap\">";
950 echo "<b>$strcategory:</b>&nbsp;";
951 echo "</td><td>";
952 popup_form ("edit.php?cat=", $catmenu, "catmenu", $current, "", "", "", false, "self");
953 echo "</td><td align=\"right\">";
954 echo "<form method=\"get\" action=\"category.php\">";
955 echo "<input type=\"hidden\" name=\"id\" value=\"$course->id\" />";
956 echo "<input type=\"submit\" value=\"$streditcats\" />";
957 echo "</form>";
958 echo '</td></tr></table>';
959 echo '<form method="get" action="edit.php" name="recurse">';
960 print_string('recurse', 'quiz');
961 echo '<input type="hidden" name="recurse" value="0">';
962 echo '<input type="checkbox" name="recurse" value="1"';
963 if ($recurse) {
964 echo ' checked="checked"';
965 }
966 echo ' onclick="document.recurse.submit(); return true;">';
967 echo '</form>';
968}
969
970
971function add_indented_names(&$categories, $id = 0, $indent = 0) {
972// returns the categories with their names indented to show parent-child relationships
973 $fillstr = '&nbsp;&nbsp;&nbsp;';
974 $fill = str_repeat($fillstr, $indent);
975 $children = array();
976 $keys = array_keys($categories);
977
978 foreach ($keys as $key) {
979 if (!isset($categories[$key]->processed) && $categories[$key]->parent == $id) {
980 $children[$key] = $categories[$key];
981 $children[$key]->indentedname = $fill . $children[$key]->name;
982 $categories[$key]->processed = true;
983 $children = $children + add_indented_names($categories, $children[$key]->id, $indent + 1);
984 }
985 }
986 return $children;
987}
988
989
990function quiz_category_select_menu($courseid,$published=false,$only_editable=false,$selected="") {
991/// displays a select menu of categories with appended coursenames
992/// optionaly non editable categories may be excluded
993/// added Howard Miller June '04
994 // get sql fragment for published
995 $publishsql="";
996 if ($published) {
997 $publishsql = "or publish=1";
998 }
999
1000 if (!isadmin()) {
1001 $categories = get_records_select("quiz_categories","course=$courseid $publishsql", 'parent, sortorder, name ASC');
1002 } else {
1003 $categories = get_records_select("quiz_categories", '', 'parent, sortorder, name ASC');
1004 }
1005
1006 $categories = add_indented_names($categories);
1007
1008 echo "<select name=\"category\">\n";
1009 foreach ($categories as $category) {
1010 $cid = $category->id;
1011 $cname = quiz_get_category_coursename($category, $courseid);
1012 $seltxt = "";
1013 if ($cid==$selected) {
1014 $seltxt = "selected=\"selected\"";
1015 }
1016 if ((!$only_editable) || isteacheredit($category->course)) {
1017 echo " <option value=\"$cid\" $seltxt>$cname</option>\n";
1018 }
1019 }
1020 echo "</select>\n";
1021}
1022
1023function quiz_get_category_coursename($category, $courseid = 0) {
1024/// if the category is not from this course and is published , adds on the course
1025/// name
1026 $cname = (isset($category->indentedname)) ? $category->indentedname : $category->name;
1027 if ($category->course != $courseid && $category->publish) {
1028 if ($catcourse=get_record("course","id",$category->course)) {
1029 $cname .= " ($catcourse->shortname) ";
1030 }
1031 }
1032 return $cname;
1033}
1034
1035function quiz_get_all_question_grades($questionlist, $quizid) {
1036// Given a list of question IDs, finds grades or invents them to
1037// create an array of matching grades
1038
1039 if (empty($questionlist)) {
1040 return array();
1041 }
1042
1043 $questions = quiz_get_question_grades($quizid, $questionlist);
1044
1045 $list = explode(",", $questionlist);
1046 $grades = array();
1047
1048 foreach ($list as $qid) {
1049 if (isset($questions[$qid])) {
1050 $grades[$qid] = $questions[$qid]->grade;
1051 } else {
1052 $grades[$qid] = 1;
1053 }
1054 }
1055 return $grades;
1056}
1057
1058function quiz_gradesmenu_options($defaultgrade) {
1059// Especially for multianswer questions it is often
1060// desirable to have the grade of the question in a quiz
1061// larger than the earlier maximum of 10 points.
1062// This function makes quiz question list grade selector drop-down
1063// have the maximum grade option set to the highest value between 10
1064// and the defaultgrade of the question.
1065
1066 if ($defaultgrade && $defaultgrade>10) {
1067 $maxgrade = $defaultgrade;
1068 } else {
1069 $maxgrade = 10;
1070 }
1071
1072 unset($gradesmenu);
1073 for ($i=$maxgrade ; $i>=0 ; --$i) {
1074 $gradesmenu[$i] = $i;
1075 }
1076 return $gradesmenu;
1077}
1078
1079function quiz_print_question_list($questionlist, $grades) {
1080// Prints a list of quiz questions in a small layout form with knobs
1081// returns sum of maximum grades
1082// $questionlist is comma-separated list
1083// $grades is an array of corresponding grades
1084
1085 global $THEME;
1086
1087 if (!$questionlist) {
1088 echo "<p align=\"center\">";
1089 print_string("noquestions", "quiz");
1090 echo "</p>";
1091 return;
1092 }
1093
1094 $order = explode(",", $questionlist);
1095
1096 if (!$questions = get_list_of_questions($questionlist)) {
1097 echo "<p align=\"center\">";
1098 print_string("noquestions", "quiz");
1099 echo "</p>";
1100 return;
1101
1102 }
1103
1104 $strorder = get_string("order");
1105 $strquestionname = get_string("questionname", "quiz");
1106 $strgrade = get_string("grade");
1107 $strdelete = get_string("delete");
1108 $stredit = get_string("edit");
1109 $strmoveup = get_string("moveup");
1110 $strmovedown = get_string("movedown");
1111 $strsavegrades = get_string("savegrades", "quiz");
1112 $strtype = get_string("type", "quiz");
1113 $strpreview = get_string("preview", "quiz");
1114
1115 $count = 0;
1116 $sumgrade = 0;
1117 $total = count($order);
1118 echo "<form method=\"post\" action=\"edit.php\">";
1119 echo "<table border=\"0\" cellpadding=\"5\" cellspacing=\"2\" width=\"100%\">\n";
1120 echo "<tr><th width=\"*\" colspan=\"3\" nowrap=\"nowrap\">$strorder</th><th align=\"left\" width=\"100%\" nowrap=\"nowrap\">$strquestionname</th><th width=\"*\" nowrap=\"nowrap\">$strtype</th><th width=\"*\" nowrap=\"nowrap\">$strgrade</th><th width=\"*\" nowrap=\"nowrap\">$stredit</th></tr>\n";
1121 foreach ($order as $qnum) {
1122 if (empty($questions[$qnum])) {
1123 continue;
1124 }
1125 $question = $questions[$qnum];
1126 $canedit = isteacheredit($question->course);
1127 $count++;
1128 echo "<tr bgcolor=\"$THEME->cellcontent\">";
1129 echo "<td>$count</td>";
1130 echo "<td>";
1131 if ($count != 1) {
1132 echo "<a title=\"$strmoveup\" href=\"edit.php?up=$qnum\"><img
1133 src=\"../../pix/t/up.gif\" border=\"0\" alt=\"$strmoveup\" /></a>";
1134 }
1135 echo "</td>";
1136 echo "<td>";
1137 if ($count != $total) {
1138 echo "<a title=\"$strmovedown\" href=\"edit.php?down=$qnum\"><img
1139 src=\"../../pix/t/down.gif\" border=\"0\" alt=\"$strmovedown\" /></a>";
1140 }
1141 echo "</td>";
1142 echo "<td>$question->name</td>";
1143 echo "<td align=\"center\">";
1144 quiz_print_question_icon($question, $canedit);
1145 echo "</td>";
1146 echo "<td>";
1147 if ($question->qtype == DESCRIPTION) {
1148 echo "<input type=\"hidden\" name=\"q$qnum\" value=\"0\" /> \n";
1149 } else {
1150 choose_from_menu(quiz_gradesmenu_options($question->defaultgrade),
1151 "q$qnum", (string)$grades[$qnum], "");
1152 }
1153 echo "<td>";
1154 echo "<a title=\"$strdelete\" href=\"edit.php?delete=$qnum\"><img
1155 src=\"../../pix/t/delete.gif\" border=\"0\" alt=\"$strdelete\" /></a>&nbsp;";
1156 echo "<a title=\"$strpreview\" href=\"#\" onClick=\"openpopup('/mod/quiz/preview.php?id=$qnum','$strpreview','scrollbars=yes,resizable=yes,width=700,height=480', false)\"><img
1157 src=\"../../pix/t/preview.gif\" border=\"0\" alt=\"$strpreview\" /></a>&nbsp;";
1158
1159 if ($canedit) {
1160 echo "<a title=\"$stredit\" href=\"question.php?id=$qnum\"><img
1161 src=\"../../pix/t/edit.gif\" border=\"0\" alt=\"$stredit\" /></a>\n";
1162 }
1163 echo "</td>";
1164
1165 $sumgrade += $grades[$qnum];
1166 }
1167 echo "<tr><td colspan=\"5\" align=\"right\">\n";
1168 echo "<input type=\"submit\" value=\"$strsavegrades:\" />\n";
1169 echo "<input type=\"hidden\" name=\"setgrades\" value=\"save\" />\n";
1170 echo "<td align=\"left\" bgcolor=\"$THEME->cellcontent\">\n";
1171 echo "<b>$sumgrade</b>";
1172 echo "</td><td>\n</td></tr>\n";
1173 echo "</table>\n";
1174 echo "</form>\n";
1175
1176 return $sumgrade;
1177}
1178
1179
1180function quiz_print_cat_question_list($categoryid, $quizselected=true, $recurse=1, $page, $perpage) {
1181// Prints the table of questions in a category with interactions
1182
1183 global $THEME, $QUIZ_QUESTION_TYPE;
1184
1185 $strcategory = get_string("category", "quiz");
1186 $strquestion = get_string("question", "quiz");
1187 $straddquestions = get_string("addquestions", "quiz");
1188 $strimportquestions = get_string("importquestions", "quiz");
1189 $strexportquestions = get_string("exportquestions", "quiz");
1190 $strnoquestions = get_string("noquestions", "quiz");
1191 $strselect = get_string("select", "quiz");
1192 $strselectall = get_string("selectall", "quiz");
1193 $strcreatenewquestion = get_string("createnewquestion", "quiz");
1194 $strquestionname = get_string("questionname", "quiz");
1195 $strdelete = get_string("delete");
1196 $stredit = get_string("edit");
1197 $straddselectedtoquiz = get_string("addselectedtoquiz", "quiz");
1198 $strtype = get_string("type", "quiz");
1199 $strcreatemultiple = get_string("createmultiple", "quiz");
1200 $strpreview = get_string("preview","quiz");
1201
1202 if (!$categoryid) {
1203 echo "<p align=\"center\"><b>";
1204 print_string("selectcategoryabove", "quiz");
1205 echo "</b></p>";
1206 if ($quizselected) {
1207 echo "<p>";
1208 print_string("addingquestions", "quiz");
1209 echo "</p>";
1210 }
1211 return;
1212 }
1213
1214 if (!$category = get_record("quiz_categories", "id", "$categoryid")) {
1215 notify("Category not found!");
1216 return;
1217 }
1218 echo "<center>";
1219 echo format_text($category->info, FORMAT_MOODLE);
1220
1221 echo '<table><tr>';
1222
1223 // check if editing of this category is allowed
1224 if (isteacheredit($category->course)) {
1225 echo "<td valign=\"top\"><b>$strcreatenewquestion:</b></td>";
1226 echo '<td valign="top" align="right">';
1227 popup_form ("question.php?category=$category->id&amp;qtype=", $QUIZ_QUESTION_TYPE, "addquestion",
1228 "", "choose", "", "", false, "self");
1229 echo '<td width="10" valign="top" align="right">';
1230 helpbutton("questiontypes", $strcreatenewquestion, "quiz");
1231 echo '</td></tr>';
1232 }
1233 else {
1234 echo '<tr><td>';
1235 print_string("publishedit","quiz");
1236 echo '</td></tr>';
1237 }
1238
1239 echo '<tr><td colspan="3" align="right"><font size="2">';
1240 if (isteacheredit($category->course)) {
1241 echo '<a href="import.php?category='.$category->id.'">'.$strimportquestions.'</a>';
1242 helpbutton("import", $strimportquestions, "quiz");
1243 echo ' | ';
1244 }
1245 echo '<a href="export.php?category='.$category->id.'">'.$strexportquestions.'</a>';
1246 helpbutton("export", $strexportquestions, "quiz");
1247 echo '</font></td></tr>';
1248
1249 echo '</table>';
1250
1251 echo '</center>';
1252
1253 $categorylist = ($recurse) ? quiz_categorylist($category->id) : $category->id;
1254
1255 if (!$questions = get_records_select('quiz_questions', "category IN ($categorylist) AND qtype != '".RANDOM."'", 'qtype, name ASC', '*', $page*$perpage, $perpage)) {
1256 echo "<p align=\"center\">";
1257 print_string("noquestions", "quiz");
1258 echo "</p>";
1259 return;
1260 }
1261
1262 $canedit = isteacheredit($category->course);
1263
1264 echo "<form method=\"post\" action=\"edit.php\">";
1265 echo "<table border=\"0\" cellpadding=\"5\" cellspacing=\"2\" width=\"100%\">";
1266 echo "<tr>";
1267 if ($quizselected) {
1268 echo "<th width=\"*\" nowrap=\"nowrap\">$strselect</th>";
1269 }
1270 echo "<th width=\"100%\" align=\"left\" nowrap=\"nowrap\">$strquestionname</th><th width=\"*\" nowrap=\"nowrap\">$strtype</th>";
1271 if ($canedit) {
1272 echo "<th width=\"*\" nowrap=\"nowrap\">$stredit</th>";
1273 }
1274 echo "</tr>\n";
1275 foreach ($questions as $question) {
1276 if ($question->qtype == RANDOM) {
1277 //continue;
1278 }
1279 echo "<tr bgcolor=\"$THEME->cellcontent\">\n";
1280 if ($quizselected) {
1281 echo "<td align=\"center\">";
1282 echo "<input type=\"checkbox\" name=\"q$question->id\" value=\"1\" />\n";
1283 echo "</td>";
1284 }
1285 echo "<td>".$question->name."</td>\n";
1286 echo "<td align=\"center\">\n";
1287 quiz_print_question_icon($question, $canedit);
1288 echo "</td>\n";
1289 if ($canedit) {
1290 echo "<td>\n";
1291 echo "<a title=\"$strdelete\" href=\"question.php?id=$question->id&amp;delete=$question->id\">\n<img
1292 src=\"../../pix/t/delete.gif\" border=\"0\" alt=\"$strdelete\" /></a>&nbsp;";
1293 echo "<a title=\"$strpreview\" href=\"#\" onClick=\"openpopup('/mod/quiz/preview.php?id=$question->id','$strpreview','scrollbars=yes,resizable=yes,width=700,height=480', false)\"><img
1294 src=\"../../pix/t/preview.gif\" border=\"0\" alt=\"$strpreview\" /></a>&nbsp;";
1295 echo "<a title=\"$stredit\" href=\"question.php?id=$question->id\"><img
1296 src=\"../../pix/t/edit.gif\" border=\"0\" alt=\"$stredit\" /></a>";
1297 echo "</td>\n";
1298 }
1299 echo "</tr>\n";
1300 }
1301 $numquestions = count_records_select('quiz_questions', "category IN ($categorylist) AND qtype != '".RANDOM."'");
1302 echo '<tr><td colspan="3">';
1303 print_paging_bar($numquestions, $page, $perpage,
1304 "edit.php?perpage=$perpage&amp;");
1305 echo '</td></tr>';
1306
1307 if ($quizselected) {
1308 echo "<tr>\n<td colspan=\"3\">";
1309 echo "<input type=\"submit\" name=\"add\" value=\"<< $straddselectedtoquiz\" />\n";
1310 //echo "<input type=\"submit\" name=\"delete\" value=\"XX Delete selected\">";
1311 echo "<input type=\"button\" onclick=\"checkall()\" value=\"$strselectall\" />\n";
1312 echo "</td></tr>";
1313 }
1314 echo "</table>\n";
1315 echo "</form>\n";
1316 if ($quizselected and isteacheredit($category->course)) {
1317 for ($i=1;$i<=10; $i++) {
1318 $randomcount[$i] = $i;
1319 }
1320 echo '<form method="post" action="multiple.php">';
1321 print_string('addrandom1', 'quiz');
1322 choose_from_menu($randomcount, 'randomcreate', '10', '');
1323 print_string('addrandom2', 'quiz');
1324 // Don't offer the option to change the grade
1325 //choose_from_menu($randomcount, 'randomgrade', '1', '');
1326 echo '<input type="hidden" name="randomgrade" value="1" />';
1327 echo '<input type="hidden" name="recurse" value="'.$recurse.'" />';
1328 echo "<input type=\"hidden\" name=\"category\" value=\"$category->id\" />";
1329 echo ' <input type="submit" name="save" value="'. get_string('add') .'" />';
1330 helpbutton('random', get_string('random', 'quiz'), 'quiz');
1331 echo '</form>';
1332 }
1333}
1334
1335
1336function quiz_start_attempt($quizid, $userid, $numattempt) {
1337 $attempt->quiz = $quizid;
1338 $attempt->userid = $userid;
1339 $attempt->attempt = $numattempt;
1340 $attempt->timestart = time();
1341 $attempt->timefinish = 0;
1342 $attempt->timemodified = time();
1343 $attempt->id = insert_record("quiz_attempts", $attempt);
1344 return $attempt;
1345}
1346
1347function quiz_get_user_attempt_unfinished($quizid, $userid) {
1348// Returns an object containing an unfinished attempt (if there is one)
1349 return get_record("quiz_attempts", "quiz", $quizid, "userid", $userid, "timefinish", 0);
1350}
1351
1352function quiz_get_user_attempts($quizid, $userid) {
1353// Returns a list of all attempts by a user
1354 return get_records_select("quiz_attempts", "quiz = '$quizid' AND userid = '$userid' AND timefinish > 0",
1355 "attempt ASC");
1356}
1357
1358
1359function quiz_get_user_attempts_string($quiz, $attempts, $bestgrade) {
1360/// Returns a simple little comma-separated list of all attempts,
1361/// with each grade linked to the feedback report and with the best grade highlighted
1362
1363 $bestgrade = format_float($bestgrade);
1364 foreach ($attempts as $attempt) {
1365 $attemptgrade = format_float(($attempt->sumgrades / $quiz->sumgrades) * $quiz->grade);
1366 if ($attemptgrade == $bestgrade) {
1367 $userattempts[] = "<span class=\"highlight\"><a href=\"review.php?q=$quiz->id&amp;attempt=$attempt->id\">$attemptgrade</a></span>";
1368 } else {
1369 $userattempts[] = "<a href=\"review.php?q=$quiz->id&amp;attempt=$attempt->id\">$attemptgrade</a>";
1370 }
1371 }
1372 return implode(",", $userattempts);
1373}
1374
1375function quiz_get_best_grade($quizid, $userid) {
1376/// Get the best current grade for a particular user in a quiz
1377 if (!$grade = get_record("quiz_grades", "quiz", $quizid, "userid", $userid)) {
1378 return "";
1379 }
1380
1381 return (round($grade->grade,0));
1382}
1383
1384function quiz_save_best_grade($quiz, $userid) {
1385/// Calculates the best grade out of all attempts at a quiz for a user,
1386/// and then saves that grade in the quiz_grades table.
1387
1388 if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) {
1389 notify("Could not find any user attempts");
1390 return false;
1391 }
1392
1393 $bestgrade = quiz_calculate_best_grade($quiz, $attempts);
1394 $bestgrade = (($bestgrade / $quiz->sumgrades) * $quiz->grade);
1395
1396 if ($grade = get_record("quiz_grades", "quiz", $quiz->id, "userid", $userid)) {
1397 $grade->grade = round($bestgrade, 2);
1398 $grade->timemodified = time();
1399 if (!update_record("quiz_grades", $grade)) {
1400 notify("Could not update best grade");
1401 return false;
1402 }
1403 } else {
1404 $grade->quiz = $quiz->id;
1405 $grade->userid = $userid;
1406 $grade->grade = round($bestgrade, 2);
1407 $grade->timemodified = time();
1408 if (!insert_record("quiz_grades", $grade)) {
1409 notify("Could not insert new best grade");
1410 return false;
1411 }
1412 }
1413 return true;
1414}
1415
1416
1417function quiz_calculate_best_grade($quiz, $attempts) {
1418/// Calculate the best grade for a quiz given a number of attempts by a particular user.
1419
1420 switch ($quiz->grademethod) {
1421
1422 case ATTEMPTFIRST:
1423 foreach ($attempts as $attempt) {
1424 return $attempt->sumgrades;
1425 }
1426 break;
1427
1428 case ATTEMPTLAST:
1429 foreach ($attempts as $attempt) {
1430 $final = $attempt->sumgrades;
1431 }
1432 return $final;
1433
1434 case GRADEAVERAGE:
1435 $sum = 0;
1436 $count = 0;
1437 foreach ($attempts as $attempt) {
1438 $sum += $attempt->sumgrades;
1439 $count++;
1440 }
1441 return (float)$sum/$count;
1442
1443 default:
1444 case GRADEHIGHEST:
1445 $max = 0;
1446 foreach ($attempts as $attempt) {
1447 if ($attempt->sumgrades > $max) {
1448 $max = $attempt->sumgrades;
1449 }
1450 }
1451 return $max;
1452 }
1453}
1454
1455
1456function quiz_calculate_best_attempt($quiz, $attempts) {
1457/// Return the attempt with the best grade for a quiz
1458
1459 switch ($quiz->grademethod) {
1460
1461 case ATTEMPTFIRST:
1462 foreach ($attempts as $attempt) {
1463 return $attempt;
1464 }
1465 break;
1466
1467 case GRADEAVERAGE: // need to do something with it :-)
1468 case ATTEMPTLAST:
1469 foreach ($attempts as $attempt) {
1470 $final = $attempt;
1471 }
1472 return $final;
1473
1474 default:
1475 case GRADEHIGHEST:
1476 $max = -1;
1477 foreach ($attempts as $attempt) {
1478 if ($attempt->sumgrades > $max) {
1479 $max = $attempt->sumgrades;
1480 $maxattempt = $attempt;
1481 }
1482 }
1483 return $maxattempt;
1484 }
1485}
1486
1487
1488function quiz_save_attempt($quiz, $questions, $result,
1489 $attemptnum, $finished = true) {
1490/// Given a quiz, a list of attempted questions and a total grade
1491/// this function saves EVERYTHING so it can be reconstructed later
1492/// if necessary.
1493
1494 global $USER;
1495 global $QUIZ_QTYPES;
1496
1497 // First find the attempt in the database (start of attempt)
1498
1499 if (!$attempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
1500 notify("Trying to save an attempt that was not started!");
1501 return false;
1502 }
1503
1504 // Not usually necessary, but there's some sort of very rare glitch
1505 // I've seen where the number wasn't already the same. In these cases
1506 // We upgrade the database to match the attemptnum we calculated
1507 $attempt->attempt = $attemptnum;
1508
1509 // Now let's complete this record and save it
1510
1511 $attempt->sumgrades = $result->sumgrades;
1512 if ($finished) {
1513 $attempt->timefinish = time();
1514 }
1515 $attempt->timemodified = time();
1516
1517 if (!update_record("quiz_attempts", $attempt)) {
1518 notify("Error while saving attempt");
1519 return false;
1520 }
1521
1522 // Now let's save all the questions for this attempt
1523
1524 foreach ($questions as $question) {
1525
1526 // Fetch the response record for this question...
1527 $response = get_record('quiz_responses',
1528 'attempt', $attempt->id, 'question', $question->id);
1529
1530 $response->grade = $result->details[$question->id]->grade;
1531
1532 if (!empty($question->response)) {
1533 $responseanswerfield = $QUIZ_QTYPES[$question->qtype]
1534 ->convert_to_response_answer_field($question->response);
1535
1536 $response->answer = $responseanswerfield;
1537
1538 } else if (!isset($response->answer)) {
1539 $response->answer = '';
1540 }
1541
1542 if (!update_record("quiz_responses", $response)) {
1543 notify("Error while saving response");
1544 return false;
1545 }
1546 }
1547 return $attempt;
1548}
1549
1550function quiz_extract_correctanswers($answers, $nameprefix) {
1551/// Convinience function that is used by some single-response
1552/// question-types for determining correct answers.
1553
1554 $bestanswerfraction = 0.0;
1555 $correctanswers = array();
1556 foreach ($answers as $answer) {
1557 if ($answer->fraction > $bestanswerfraction) {
1558 $correctanswers = array($nameprefix.$answer->id => $answer);
1559 $bestanswerfraction = $answer->fraction;
1560 } else if ($answer->fraction == $bestanswerfraction) {
1561 $correctanswers[$nameprefix.$answer->id] = $answer;
1562 }
1563 }
1564 return $correctanswers;
1565}
1566
1567function quiz_grade_responses($quiz, $questions, $attemptid=0) {
1568/// Given a list of questions (including ->response[] and ->maxgrade
1569/// on each question) this function does all the hard work of calculating the
1570/// score for each question, as well as a total grade for
1571/// the whole quiz. It returns everything in a structure
1572/// that lookas like this
1573/// ->sumgrades (sum of all grades for all questions)
1574/// ->grade (final grade result for the whole quiz)
1575/// ->percentage (Percentage of the max grade achieved)
1576/// ->details[]
1577/// The array ->details[] is indexed like the $questions argument
1578/// and contains scoring information per question. Each element has
1579/// this structure:
1580/// []->grade (Grade awarded on the specific question)
1581/// []->answers[] (result answer records for the question response(s))
1582/// []->correctanswers[] (answer records if question response(s) had been correct)
1583/// - HOWEVER, ->answers[] and ->correctanswers[] are supplied only
1584/// if there is a response on the question...
1585/// The array ->answers[] is indexed like ->response[] on its corresponding
1586/// element in $questions. It is the case for ->correctanswers[] when
1587/// there can be multiple responses per question but if there can be only one
1588/// response per question then all possible correctanswers will be
1589/// represented, indexed like the response index concatenated with the ->id
1590/// of its answer record.
1591
1592 global $QUIZ_QTYPES;
1593
1594 if (!$questions) {
1595 error("No questions!");
1596 }
1597
1598 $result->sumgrades = 0.0;
1599 foreach ($questions as $qid => $question) {
1600
1601 if (!isset($question->response) && $attemptid) {
1602 /// No response on the question
1603 /// This case is common if the quiz shows a limited
1604 /// number of questions per page.
1605 $response = get_record('quiz_responses', 'attempt',
1606 $attemptid, 'question', $qid);
1607 $resultdetails->grade = $response->grade;
1608
1609 } else if (empty($question->qtype)) {
1610 continue;
1611
1612 } else {
1613
1614 $resultdetails = $QUIZ_QTYPES[$question->qtype]->grade_response
1615 ($question, quiz_qtype_nameprefix($question));
1616
1617 // Negative grades will not do:
1618 if (((float)($resultdetails->grade)) <= 0.0) {
1619 $resultdetails->grade = 0.0;
1620
1621 // Neither will extra credit:
1622 } else if (((float)($resultdetails->grade)) >= 1.0) {
1623 $resultdetails->grade = $question->maxgrade;
1624
1625 } else {
1626 $resultdetails->grade *= $question->maxgrade;
1627 }
1628 }
1629
1630 // if time limit is enabled and exceeded, return zero grades
1631 if ($quiz->timelimit > 0) {
1632 if (($quiz->timelimit + 60) <= $quiz->timesincestart) {
1633 $resultdetails->grade = 0;
1634 }
1635 }
1636
1637 $result->sumgrades += $resultdetails->grade;
1638 $resultdetails->grade = round($resultdetails->grade, 2);
1639 $result->details[$qid] = $resultdetails;
1640 }
1641
1642 $fraction = (float)($result->sumgrades / $quiz->sumgrades);
1643 $result->percentage = format_float($fraction * 100.0);
1644 $result->grade = format_float($fraction * $quiz->grade);
1645 $result->sumgrades = round($result->sumgrades, 2);
1646
1647 return $result;
1648}
1649
1650function quiz_get_recent_mod_activity(&$activities, &$index, $sincetime, $courseid, $quiz="0", $user="", $groupid="") {
1651// Returns all quizzes since a given time. If quiz is specified then
1652// this restricts the results
1653
1654 global $CFG;
1655
1656 if ($quiz) {
1657 $quizselect = " AND cm.id = '$quiz'";
1658 } else {
1659 $quizselect = "";
1660 }
1661 if ($user) {
1662 $userselect = " AND u.id = '$user'";
1663 } else {
1664 $userselect = "";
1665 }
1666
1667 $quizzes = get_records_sql("SELECT qa.*, q.name, u.firstname, u.lastname, u.picture,
1668 q.course, q.sumgrades as maxgrade, cm.instance, cm.section
1669 FROM {$CFG->prefix}quiz_attempts qa,
1670 {$CFG->prefix}quiz q,
1671 {$CFG->prefix}user u,
1672 {$CFG->prefix}course_modules cm
1673 WHERE qa.timefinish > '$sincetime'
1674 AND qa.userid = u.id $userselect
1675 AND qa.quiz = q.id $quizselect
1676 AND cm.instance = q.id
1677 AND cm.course = '$courseid'
1678 AND q.course = cm.course
1679 ORDER BY qa.timefinish ASC");
1680
1681 if (empty($quizzes))
1682 return;
1683
1684 foreach ($quizzes as $quiz) {
1685 if (empty($groupid) || ismember($groupid, $quiz->userid)) {
1686
1687 $tmpactivity->type = "quiz";
1688 $tmpactivity->defaultindex = $index;
1689 $tmpactivity->instance = $quiz->quiz;
1690
1691 $tmpactivity->name = $quiz->name;
1692 $tmpactivity->section = $quiz->section;
1693
1694 $tmpactivity->content->attemptid = $quiz->id;
1695 $tmpactivity->content->sumgrades = $quiz->sumgrades;
1696 $tmpactivity->content->maxgrade = $quiz->maxgrade;
1697 $tmpactivity->content->attempt = $quiz->attempt;
1698
1699 $tmpactivity->user->userid = $quiz->userid;
1700 $tmpactivity->user->fullname = fullname($quiz);
1701 $tmpactivity->user->picture = $quiz->picture;
1702
1703 $tmpactivity->timestamp = $quiz->timefinish;
1704
1705 $activities[] = $tmpactivity;
1706
1707 $index++;
1708 }
1709 }
1710
1711 return;
1712}
1713
1714
1715function quiz_print_recent_mod_activity($activity, $course, $detail=false) {
1716 global $CFG, $THEME;
1717
1718 echo '<table border="0" cellpadding="3" cellspacing="0">';
1719
1720 echo "<tr><td bgcolor=\"$THEME->cellcontent2\" class=\"forumpostpicture\" width=\"35\" valign=\"top\">";
1721 print_user_picture($activity->user->userid, $course, $activity->user->picture);
1722 echo "</td><td width=\"100%\"><font size=\"2\">";
1723
1724 if ($detail) {
1725 echo "<img src=\"$CFG->modpixpath/$activity->type/icon.gif\" ".
1726 "height=\"16\" width=\"16\" alt=\"$activity->type\" /> ";
1727 echo "<a href=\"$CFG->wwwroot/mod/quiz/view.php?id=" . $activity->instance . "\">"
1728 . $activity->name . "</a> - ";
1729
1730 }
1731
1732 if (isteacher($USER)) {
1733 $grades = "(" . $activity->content->sumgrades . " / " . $activity->content->maxgrade . ") ";
1734 echo "<a href=\"$CFG->wwwroot/mod/quiz/review.php?q="
1735 . $activity->instance . "&amp;attempt="
1736 . $activity->content->attemptid . "\">" . $grades . "</a> ";
1737
1738 echo get_string("attempt", "quiz") . " - " . $activity->content->attempt . "<br />";
1739 }
1740 echo "<a href=\"$CFG->wwwroot/user/view.php?id="
1741 . $activity->user->userid . "&amp;course=$course\">"
1742 . $activity->user->fullname . "</a> ";
1743
1744 echo " - " . userdate($activity->timestamp);
1745
1746 echo "</font></td></tr>";
1747 echo "</table>";
1748
1749 return;
1750}
1751
1752// this function creates default export filename
1753function default_export_filename($course,$category) {
1754 //Take off some characters in the filename !!
1755 $takeoff = array(" ", ":", "/", "\\", "|");
1756 $export_word = str_replace($takeoff,"_",strtolower(get_string("exportfilename","quiz")));
1757 //If non-translated, use "export"
1758 if (substr($export_word,0,1) == "[") {
1759 $export_word= "export";
1760 }
1761
1762 //Calculate the date format string
1763 $export_date_format = str_replace(" ","_",get_string("exportnameformat","quiz"));
1764 //If non-translated, use "%Y%m%d-%H%M"
1765 if (substr($export_date_format,0,1) == "[") {
1766 $export_date_format = "%%Y%%m%%d-%%H%%M";
1767 }
1768
1769 //Calculate the shortname
1770 $export_shortname = clean_filename($course->shortname);
1771 if (empty($export_shortname) or $export_shortname == '_' ) {
1772 $export_shortname = $course->id;
1773 }
1774
1775 //Calculate the category name
1776 $export_categoryname = clean_filename($category->name);
1777
1778 //Calculate the final export filename
1779 //The export word
1780 $export_name = $export_word."-";
1781 //The shortname
1782 $export_name .= strtolower($export_shortname)."-";
1783 //The category name
1784 $export_name .= strtolower($export_categoryname)."-";
1785 //The date format
1786 $export_name .= userdate(time(),$export_date_format,99,false);
1787 //The extension - no extension, supplied by format
1788 // $export_name .= ".txt";
1789
1790 return $export_name;
1791}
1792
1793// function to read all questions for category into big array
1794// added by Howard Miller June 2004
1795function get_questions_category( $category ) {
1796
1797 // questions will be added to an array
1798 $qresults = array();
1799
1800 // get the list of questions for the category
1801 if ($questions = get_records("quiz_questions","category",$category->id)) {
1802
1803 // iterate through questions, getting stuff we need
1804 foreach($questions as $question) {
1805 $new_question = get_question_data( $question );
1806 $qresults[] = $new_question;
1807 }
1808 }
1809
1810 return $qresults;
1811}
1812
1813// function to read single question, parameter is object view of
1814// quiz_categories record, results is a combined object
1815// defined as follows...
1816// ->id quiz_questions id
1817// ->category category
1818// ->name q name
1819// ->questiontext
1820// ->image
1821// ->qtype see defines at the top of this file
1822// ->stamp not too sure
1823// ->version not sure
1824// ----SHORTANSWER
1825// ->usecase
1826// ->answers array of answers
1827// ----TRUEFALSE
1828// ->trueanswer truefalse answer
1829// ->falseanswer truefalse answer
1830// ----MULTICHOICE
1831// ->layout
1832// ->single many or just one correct answer
1833// ->answers array of answer objects
1834// ----NUMERIC
1835// ->min minimum answer span
1836// ->max maximum answer span
1837// ->answer single answer
1838// ----MATCH
1839// ->subquestions array of sub questions
1840// ---->questiontext
1841// ---->answertext
1842function get_question_data( $question ) {
1843 // what to do next depends of question type (qtype)
1844 switch ($question->qtype) {
1845 case SHORTANSWER:
1846 $shortanswer = get_record("quiz_shortanswer","question",$question->id);
1847 $question->usecase = $shortanswer->usecase;
1848 $question->answers = get_exp_answers( $question->id );
1849 break;
1850 case TRUEFALSE:
1851 if (!$truefalse = get_record("quiz_truefalse","question",$question->id)) {
1852 error( "quiz_truefalse record $question->id not found" );
1853 }
1854 $question->trueanswer = get_exp_answer( $truefalse->trueanswer );
1855 $question->falseanswer = get_exp_answer( $truefalse->falseanswer );
1856 break;
1857 case MULTICHOICE:
1858 if (!$multichoice = get_record("quiz_multichoice","question",$question->id)) {
1859 error( "quiz_multichoice $question->id not found" );
1860 }
1861 $question->layout = $multichoice->layout;
1862 $question->single = $multichoice->single;
1863 $question->answers = get_exp_answers( $multichoice->question );
1864 break;
1865 case NUMERICAL:
1866 if (!$numeric = get_record("quiz_numerical","question",$question->id)) {
1867 error( "quiz_numerical $question->id not found" );
1868 }
1869 $question->min = $numeric->min;
1870 $question->max = $numeric->max;
1871 $question->answer = get_exp_answer( $numeric->answer );
1872 break;
1873 case MATCH:
1874 if (!$subquestions = get_records("quiz_match_sub","question",$question->id)) {
1875 error( "quiz_match_sub $question->id not found" );
1876 }
1877 $question->subquestions = $subquestions;
1878 break;
1879 case DESCRIPTION:
1880 // nothing to do
1881 break;
1882 case MULTIANSWER:
1883 // nothing to do
1884 break;
1885 default:
1886 notify("No handler for question type $question->qtype in get_question");
1887 }
1888 return $question;
1889}
1890
1891// function to return single answer
1892// ->id answer id
1893// ->question question number
1894// ->answer
1895// ->fraction
1896// ->feedback
1897function get_exp_answer( $id ) {
1898 if (!$answer = get_record("quiz_answers","id",$id )) {
1899 error( "quiz_answers record $id not found" );
1900 }
1901 return $answer;
1902}
1903
1904// function to return array of answers for export
1905function get_exp_answers( $question_num ) {
1906 if (!$answers = get_records("quiz_answers","question",$question_num)) {
1907 error( "quiz_answers question $question_num not found" );
1908 }
1909 return $answers;
1910}
1911
1912function quiz_categorylist($categoryid) {
1913 // returns a comma separated list of ids of the category and all subcategories
1914 $categorylist = $categoryid;
1915 if ($subcategories = get_records('quiz_categories', 'parent', $categoryid, 'sortorder ASC', 'id, id')) {
1916 foreach ($subcategories as $subcategory) {
1917 $categorylist .= ','. quiz_categorylist($subcategory->id);
1918 }
1919 }
1920 return $categorylist;
1921}
1922?>