Merged from HEAD
[moodle.git] / mod / quiz / locallib.php
blobe05d5228faa9f9f0cfde7ad68767f7091b563a90
1 <?php // $Id$
2 /**
3 * Library of functions used by the quiz module.
5 * This contains functions that are called from within the quiz module only
6 * Functions that are also called by core Moodle are in {@link lib.php}
7 * @version $Id$
8 * @author Martin Dougiamas and many others. This has recently been completely
9 * rewritten by Alex Smith, Julian Sedding and Gustav Delius as part of
10 * the Serving Mathematics project
11 * {@link http://maths.york.ac.uk/serving_maths}
12 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
13 * @package quiz
16 /**
17 * Include those library functions that are also used by core Moodle
19 require_once("$CFG->dirroot/mod/quiz/lib.php");
21 /// CONSTANTS ///////////////////////////////////////////////////////////////////
23 /**#@+
24 * Options determining how the grades from individual attempts are combined to give
25 * the overall grade for a user
27 define("QUIZ_GRADEHIGHEST", "1");
28 define("QUIZ_GRADEAVERAGE", "2");
29 define("QUIZ_ATTEMPTFIRST", "3");
30 define("QUIZ_ATTEMPTLAST", "4");
31 $QUIZ_GRADE_METHOD = array ( QUIZ_GRADEHIGHEST => get_string("gradehighest", "quiz"),
32 QUIZ_GRADEAVERAGE => get_string("gradeaverage", "quiz"),
33 QUIZ_ATTEMPTFIRST => get_string("attemptfirst", "quiz"),
34 QUIZ_ATTEMPTLAST => get_string("attemptlast", "quiz"));
35 /**#@-*/
37 /**#@+
38 * The different types of events that can create question states
40 define('QUIZ_EVENTOPEN', '0');
41 define('QUIZ_EVENTNAVIGATE', '1');
42 define('QUIZ_EVENTSAVE', '2');
43 define('QUIZ_EVENTGRADE', '3');
44 define('QUIZ_EVENTDUPLICATEGRADE', '4');
45 define('QUIZ_EVENTVALIDATE', '5');
46 define('QUIZ_EVENTCLOSE', '6');
47 /**#@-*/
49 /**#@+
50 * The defined question types
52 * @todo It would be nicer to have a fully automatic plug-in system
54 define("SHORTANSWER", "1");
55 define("TRUEFALSE", "2");
56 define("MULTICHOICE", "3");
57 define("RANDOM", "4");
58 define("MATCH", "5");
59 define("RANDOMSAMATCH", "6");
60 define("DESCRIPTION", "7");
61 define("NUMERICAL", "8");
62 define("MULTIANSWER", "9");
63 define("CALCULATED", "10");
64 define("RQP", "11");
65 /**#@-*/
68 define("QUIZ_PICTURE_MAX_HEIGHT", "600"); // Not currently implemented
69 define("QUIZ_PICTURE_MAX_WIDTH", "600"); // Not currently implemented
71 define("QUIZ_MAX_NUMBER_ANSWERS", "10");
73 define("QUIZ_CATEGORIES_SORTORDER", "999");
75 /**
76 * Array holding question type objects
78 $QUIZ_QTYPES= array();
81 /// Question type class //////////////////////////////////////////////
83 class quiz_default_questiontype {
85 /**
86 * Name of the question type
88 * The name returned should coincide with the name of the directory
89 * in which this questiontype is located
90 * @ return string
92 function name() {
93 return 'default';
96 /**
97 * Checks whether a given file is used by a particular question
99 * This is used by {@see quizfile.php} to determine whether a file
100 * should be served to the user
101 * @return boolean
102 * @param question $question
103 * @param string $relativefilepath
105 function uses_quizfile($question, $relativefilepath) {
106 // The default does only check whether the file is used as image:
107 return $question->image == $relativefilepath;
111 * Saves or updates a question after editing by a teacher
113 * Given some question info and some data about the answers
114 * this function parses, organises and saves the question
115 * It is used by {@link question.php} when saving new data from
116 * a form, and also by {@link import.php} when importing questions
117 * This function in turn calls {@link save_question_options}
118 * to save question-type specific options
119 * @return object A {@link question} object
120 * @param object $question The question object which should be updated
121 * @param object $form The form submitted by the teacher
122 * @param object $course The course we are in
124 function save_question($question, $form, $course) {
125 // This default implementation is suitable for most
126 // question types.
128 // First, save the basic question itself
130 $question->name = trim($form->name);
131 $question->questiontext = trim($form->questiontext);
132 $question->questiontextformat = $form->questiontextformat;
133 $question->parent = isset($form->parent)? $form->parent : 0;
134 $question->length = $this->actual_number_of_questions($question);
135 $question->penalty = isset($form->penalty) ? $form->penalty : 0;
137 if (empty($form->image)) {
138 $question->image = "";
139 } else {
140 $question->image = $form->image;
143 if (empty($question->name)) {
144 $question->name = strip_tags($question->questiontext);
145 if (empty($question->name)) {
146 $question->name = '-';
150 if ($question->penalty > 1 or $question->penalty < 0) {
151 $question->errors['penalty'] = get_string('invalidpenalty', 'quiz');
154 if (isset($form->defaultgrade)) {
155 $question->defaultgrade = $form->defaultgrade;
158 if (!empty($question->id)) { // Question already exists
159 $question->version ++; // Update version number of question
160 if (!update_record("quiz_questions", $question)) {
161 error("Could not update question!");
163 } else { // Question is a new one
164 $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed)
165 $question->version = 1;
166 if (!$question->id = insert_record("quiz_questions", $question)) {
167 error("Could not insert new question!");
171 // Now to save all the answers and type-specific options
173 $form->id = $question->id;
174 $form->qtype = $question->qtype;
175 $form->category = $question->category;
177 $result = $this->save_question_options($form);
179 if (!empty($result->error)) {
180 error($result->error);
183 if (!empty($result->notice)) {
184 notice($result->notice, "question.php?id=$question->id");
187 if (!empty($result->noticeyesno)) {
188 notice_yesno($result->noticeyesno, "question.php?id=$question->id", "edit.php");
189 print_footer($course);
190 exit;
193 return $question;
197 * Saves question-type specific options
199 * This is called by {@link save_question()} to save the question-type specific data
200 * @return object $result->error or $result->noticeyesno or $result->notice
201 * @param object $question This holds the information from the editing form,
202 * it is not a standard question object.
204 function save_question_options($question) {
205 /// This default implementation must be overridden:
207 $result->error = "Unsupported question type ($question->qtype)!";
208 return $result;
212 * Changes all states for the given attempts over to a new question
214 * This is used by the versioning code if the teacher requests that a question
215 * gets replaced by the new version. In order for the attempts to be regraded
216 * properly all data in the states referring to the old question need to be
217 * changed to refer to the new version instead. In particular for question types
218 * that use the answers table the answers belonging to the old question have to
219 * be changed to those belonging to the new version.
221 * @param integer $oldquestionid The id of the old question
222 * @param object $newquestion The new question
223 * @param array $attempts An array of all attempt objects in whose states
224 * replacement should take place
226 function replace_question_in_attempts($oldquestionid, $newquestion, $attemtps) {
227 echo 'Not yet implemented';
228 return;
232 * Loads the question type specific options for the question.
234 * This function loads any question type specific options for the
235 * question from the database into the question object. This information
236 * is placed in the $question->options field. A question type is
237 * free, however, to decide on a internal structure of the options field.
238 * @return bool Indicates success or failure.
239 * @param object $question The question object for the question. This object
240 * should be updated to include the question type
241 * specific information (it is passed by reference).
243 function get_question_options(&$question) {
244 if (!isset($question->options)) {
245 $question->options = new object;
247 // The default implementation attaches all answers for this question
248 if (!$question->options->answers = get_records('quiz_answers', 'question',
249 $question->id)) {
250 //notify('Error: Missing question answers!');
251 return false;
253 return true;
257 * Returns the number of question numbers which are used by the question
259 * This function returns the number of question numbers to be assigned
260 * to the question. Most question types will have length one; they will be
261 * assigned one number. The DESCRIPTION type, however does not use up a
262 * number and so has a length of zero. Other question types may wish to
263 * handle a bundle of questions and hence return a number greater than one.
264 * @return integer The number of question numbers which should be
265 * assigned to the question.
266 * @param object $question The question whose length is to be determined.
267 * Question type specific information is included.
269 function actual_number_of_questions($question) {
270 // By default, each question is given one number
271 return 1;
275 * Creates empty session and response information for the question
277 * This function is called to start a question session. Empty question type
278 * specific session data (if any) and empty response data will be added to the
279 * state object. Session data is any data which must persist throughout the
280 * quiz attempt possibly with updates as the user interacts with the
281 * question. This function does NOT create new entries in the database for
282 * the session; a call to the {@link save_session_and_responses} member will
283 * occur to do this.
284 * @return bool Indicates success or failure.
285 * @param object $question The question for which the session is to be
286 * created. Question type specific information is
287 * included.
288 * @param object $state The state to create the session for. Note that
289 * this will not have been saved in the database so
290 * there will be no id. This object will be updated
291 * to include the question type specific information
292 * (it is passed by reference). In particular, empty
293 * responses will be created in the ->responses
294 * field.
295 * @param object $quiz The quiz for which the session is to be started.
296 * Questions may wish to initialize the session in
297 * different ways depending on quiz settings.
298 * @param object $attempt The quiz attempt for which the session is to be
299 * started. Questions may wish to initialize the
300 * session in different ways depending on the user id
301 * or time available for the attempt.
303 function create_session_and_responses(&$question, &$state, $quiz, $attempt) {
304 // The default implementation should work for the legacy question types.
305 // Most question types with only a single form field for the student's response
306 // will use the empty string '' as the index for that one response. This will
307 // automatically be stored in and restored from the answer field in the
308 // quiz_states table.
309 $state->responses = array('' => '');
310 return true;
314 * Restores the session data and most recent responses for the given state
316 * This function loads any session data associated with the question
317 * session in the given state from the database into the state object.
318 * In particular it loads the responses that have been saved for the given
319 * state into the ->responses member of the state object.
321 * Question types with only a single form field for the student's response
322 * will not need not restore the responses; the value of the answer
323 * field in the quiz_states table is restored to ->responses['']
324 * before this function is called. Question types with more response fields
325 * should override this method and set the ->responses field to an
326 * associative array of responses.
327 * @return bool Indicates success or failure.
328 * @param object $question The question object for the question including any
329 * question type specific information.
330 * @param object $state The saved state to load the session for. This
331 * object should be updated to include the question
332 * type specific session information and responses
333 * (it is passed by reference).
335 function restore_session_and_responses(&$question, &$state) {
336 // The default implementation does nothing (successfully)
337 return true;
341 * Saves the session data and responses for the given question and state
343 * This function saves the question type specific session data from the
344 * state object to the database. In particular for most question types it saves the
345 * responses from the ->responses member of the state object. The question type
346 * non-specific data for the state has already been saved in the quiz_states
347 * table and the state object contains the corresponding id and
348 * sequence number which may be used to index a question type specific table.
350 * Question types with only a single form field for the student's response
351 * which is contained in ->responses[''] will not have to save this response,
352 * it will already have been saved to the answer field of the quiz_states table.
353 * Question types with more response fields should override this method and save
354 * the responses in their own database tables.
355 * @return bool Indicates success or failure.
356 * @param object $question The question object for the question including
357 * the question type specific information.
358 * @param object $state The state for which the question type specific
359 * data and responses should be saved.
361 function save_session_and_responses(&$question, &$state) {
362 // The default implementation does nothing (successfully)
363 return true;
367 * Returns an array of values which will give full marks if graded as
368 * the $state->responses field
370 * The correct answer to the question in the given state, or an example of
371 * a correct answer if there are many, is returned. This is used by some question
372 * types in the {@link grade_responses()} function but it is also used by the
373 * question preview screen to fill in correct responses.
374 * @return mixed An array of values giving the responses corresponding
375 * to the (or a) correct answer to the question. If there is
376 * no correct answer that scores 100% then null is returned.
377 * @param object $question The question for which the correct answer is to
378 * be retrieved. Question type specific information is
379 * available.
380 * @param object $state The state of the question, for which a correct answer is
381 * needed. Question type specific information is included.
383 function get_correct_responses(&$question, &$state) {
384 /* The default implementation returns the response for the first answer
385 that gives full marks. */
386 foreach ($question->options->answers as $answer) {
387 if (((int) $answer->fraction) === 1) {
388 return array('' => $answer->answer);
391 return null;
395 * Return an array of values with the texts for all possible responses stored
396 * for the question
398 * All answers are found and their text values isolated
399 * @return object A mixed object
400 * ->id question id. Needed to manage random questions:
401 * it's the id of the actual question presented to user in a given attempt
402 * ->responses An array of values giving the responses corresponding
403 * to all answers to the question. Answer ids are used as keys.
404 * The text and partial credit are the object components
405 * @param object $question The question for which the answers are to
406 * be retrieved. Question type specific information is
407 * available.
409 // ULPGC ecastro
410 function get_all_responses(&$question, &$state) {
411 unset($answers);
412 if (is_array($question->options->answers)) {
413 foreach ($question->options->answers as $aid=>$answer) {
414 unset ($r);
415 $r->answer = $answer->answer;
416 $r->credit = $answer->fraction;
417 $answers[$aid] = $r;
419 } else {
420 $answers[]="error"; // just for debugging, eliminate
422 $result->id = $question->id;
423 $result->responses = $answers;
424 return $result;
428 * Return the actual response to the question in a given state
429 * for the question
431 * @return mixed An array containing the response or reponses (multiple answer, match)
432 * given by the user in a particular attempt.
433 * @param object $question The question for which the correct answer is to
434 * be retrieved. Question type specific information is
435 * available.
436 * @param object $state The state object that corresponds to the question,
437 * for which a correct answer is needed. Question
438 * type specific information is included.
440 // ULPGC ecastro
441 function get_actual_response(&$question, &$state) {
442 /* The default implementation only returns the raw ->responses.
443 may be overridden by each type*/
444 //unset($resp);
445 if (isset($state->responses)) {
446 return $state->responses;
447 } else {
448 return null;
452 // ULPGC ecastro
453 function get_fractional_grade(&$question, &$state) {
454 $maxgrade = $question->maxgrade;
455 $grade = $state->grade;
456 if ($maxgrade) {
457 return (float)($grade/$maxgrade);
458 } else {
459 return (float)$grade;
465 * Checks if the response given is correct and returns the id
467 * @return int The ide number for the stored answer that matches the response
468 * given by the user in a particular attempt.
469 * @param object $question The question for which the correct answer is to
470 * be retrieved. Question type specific information is
471 * available.
472 * @param object $state The state object that corresponds to the question,
473 * for which a correct answer is needed. Question
474 * type specific information is included.
476 // ULPGC ecastro
477 function check_response(&$question, &$state){
478 return false;
482 * Prints the question including the number, grading details, content,
483 * feedback and interactions
485 * This function prints the question including the question number,
486 * grading details, content for the question, any feedback for the previously
487 * submitted responses and the interactions. The default implementation calls
488 * various other methods to print each of these parts and most question types
489 * will just override those methods.
490 * @todo Use CSS stylesheet
491 * @param object $question The question to be rendered. Question type
492 * specific information is included. The
493 * maximum possible grade is in ->maxgrade. The name
494 * prefix for any named elements is in ->name_prefix.
495 * @param object $state The state to render the question in. The grading
496 * information is in ->grade, ->raw_grade and
497 * ->penalty. The current responses are in
498 * ->responses. This is an associative array (or the
499 * empty string or null in the case of no responses
500 * submitted). The last graded state is in
501 * ->last_graded (hence the most recently graded
502 * responses are in ->last_graded->responses). The
503 * question type specific information is also
504 * included.
505 * @param integer $number The number for this question.
506 * @param object $quiz The quiz to which the question belongs. The
507 * question will likely be rendered differently
508 * depending on the quiz settings.
509 * @param object $options An object describing the rendering options.
511 function print_question(&$question, &$state, $number, $quiz, $options) {
512 /* The default implementation should work for most question types
513 provided the member functions it calls are overridden where required.
514 The question number is printed in the first cell of a table.
516 The main content is printed below in the top row of the second column
517 using {@link print_question_formulation_and_controls}.
518 The grading details are printed in the second row in the second column
519 using {@print_question_grading_details}.
520 The {@link print_question_submit_buttons} member is invoked to add a third
521 row containing the submit button(s) when $options->readonly is false. */
523 print_simple_box_start('center', '90%');
524 echo '<table width="100%" cellspacing="10"><tr>';
525 if ($options->readonly) {
526 echo '<td nowrap="nowrap" width="80" valign="top" rowspan="2">';
527 } else {
528 echo '<td nowrap="nowrap" width="80" valign="top" rowspan="3">';
531 // Print question number
532 echo '<b><font size="+1">' . $number . '</font></b>';
533 if (isteacher($quiz->course)) {
534 echo ' <font size="1">( ';
535 link_to_popup_window ('/mod/quiz/question.php?id=' . $question->id,
536 'editquestion', $question->id, 450, 550, get_string('edit'));
537 echo ')</font>';
539 if ($question->maxgrade and $options->scores) {
540 echo '<div class="grade">';
541 echo get_string('marks', 'quiz').': ';
542 if ($quiz->optionflags & QUIZ_ADAPTIVE) {
543 echo '<br />';
544 echo ('' === $state->last_graded->grade) ? '--/' : round($state->last_graded->grade, $quiz->decimalpoints).'/';
546 echo $question->maxgrade.'</div>';
549 echo '</td><td valign="top">';
551 $this->print_question_formulation_and_controls($question, $state,
552 $quiz, $options);
554 echo '</td></tr><tr><td valign="top">';
556 if ($question->maxgrade and $options->scores) {
557 $this->print_question_grading_details($question, $state, $quiz, $options);
560 if (QUIZ_EVENTDUPLICATEGRADE == $state->event) {
561 echo ' ';
562 print_string('duplicateresponse', 'quiz');
565 if(!$options->readonly) {
566 echo '</td></tr><tr><td align="right">';
567 $this->print_question_submit_buttons($question, $state,
568 $quiz, $options);
571 if(isset($options->history) and $options->history) {
572 if ($options->history == 'all') {
573 // show all states
574 $states = get_records_select('quiz_states', "attempt = '$state->attempt' AND question = '$question->id' AND event > '0'", 'seq_number DESC');
575 } else {
576 // show only graded states
577 $states = get_records_select('quiz_states', "attempt = '$state->attempt' AND question = '$question->id' AND event = '".QUIZ_EVENTGRADE."'", 'seq_number DESC');
579 if (count($states) > 1) {
580 $strreviewquestion = get_string('reviewresponse', 'quiz');
581 unset($table);
582 $table->head = array (
583 get_string('numberabbr', 'quiz'),
584 get_string('action', 'quiz'),
585 get_string('response', 'quiz'),
586 get_string('time'),
587 get_string('score', 'quiz'),
588 get_string('penalty', 'quiz'),
589 get_string('grade', 'quiz'),
591 $table->align = array ('center', 'center', 'left', 'left', 'left', 'left', 'left');
592 $table->size = array ('', '', '', '', '', '', '');
593 $table->width = '100%';
594 foreach ($states as $st) {
595 $b = ($state->id == $st->id) ? '<b>' : '';
596 $be = ($state->id == $st->id) ? '</b>' : '';
597 $table->data[] = array (
598 ($state->id == $st->id) ? '<b>'.$st->seq_number.'</b>' : link_to_popup_window ('/mod/quiz/reviewquestion.php?state='.$st->id.'&amp;number='.$number, 'reviewquestion', $st->seq_number, 450, 650, $strreviewquestion, 'none', true),
599 $b.get_string('event'.$st->event, 'quiz').$be,
600 $b.$this->response_summary($st).$be,
601 $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be,
602 $b.round($st->raw_grade, $quiz->decimalpoints).$be,
603 $b.round($st->penalty, $quiz->decimalpoints).$be,
604 $b.round($st->grade, $quiz->decimalpoints).$be
607 echo '</td></tr><tr><td colspan="2" valign="top">';
608 print_table($table);
611 echo '</td></tr></table>';
612 print_simple_box_end();
617 * Prints the score obtained and maximum score available plus any penalty
618 * information
620 * This function prints a summary of the scoring in the most recently
621 * graded state (the question may not have been submitted for marking at
622 * the current state). The default implementation should be suitable for most
623 * question types.
624 * @param object $question The question for which the grading details are
625 * to be rendered. Question type specific information
626 * is included. The maximum possible grade is in
627 * ->maxgrade.
628 * @param object $state The state. In particular the grading information
629 * is in ->grade, ->raw_grade and ->penalty.
630 * @param object $quiz The quiz to which the question belongs. The
631 * grading details may be rendered differently
632 * depending on the quiz settings.
633 * @param object $options An object describing the rendering options.
635 function print_question_grading_details(&$question, &$state, $quiz, $options) {
636 /* The default implementation prints the number of marks if no attempt
637 has been made. Otherwise it displays the grade obtained out of the
638 maximum grade available and a warning if a penalty was applied for the
639 attempt and displays the overall grade obtained counting all previous
640 responses (and penalties) */
642 if (!empty($question->maxgrade)) {
643 if (!('' === $state->last_graded->grade)) {
644 // Display the grading details from the last graded state
645 $grade->cur = round($state->last_graded->grade, $quiz->decimalpoints);
646 $grade->max = $question->maxgrade;
647 $grade->raw = round($state->last_graded->raw_grade, $quiz->decimalpoints);
649 echo '<div class="correctness">';
650 if ($grade->raw >= $grade->max) {
651 print_string('correct', 'quiz');
652 } else if ($grade->raw > 0) {
653 print_string('partiallycorrect', 'quiz');
654 } else {
655 print_string('incorrect', 'quiz');
657 echo '</div>';
659 echo '<div class="gradingdetails">';
660 // print grade for this submission
661 print_string('gradingdetails', 'quiz', $grade);
662 if ($quiz->penaltyscheme) {
663 // print details of grade adjustment due to penalties
664 if ($state->last_graded->raw_grade > $state->last_graded->grade){
665 print_string('gradingdetailsadjustment', 'quiz', $grade);
667 // print info about new penalty
668 // penalty is relevant only if the answer is not correct and further attempts are possible
669 if (($state->last_graded->raw_grade < $question->maxgrade) and (QUIZ_EVENTCLOSE !== $state->event)) {
670 if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) {
671 // A penalty was applied so display it
672 print_string('gradingdetailspenalty', 'quiz', $state->last_graded->penalty);
673 } else {
674 /* No penalty was applied even though the answer was
675 not correct (eg. a syntax error) so tell the student
676 that they were not penalised for the attempt */
677 print_string('gradingdetailszeropenalty', 'quiz');
681 echo '</div>';
687 * Prints the main content of the question including any interactions
689 * This function prints the main content of the question including the
690 * interactions for the question in the state given. The last graded responses
691 * are printed or indicated and the current responses are selected or filled in.
692 * Any names (eg. for any form elements) are prefixed with $question->name_prefix.
693 * This method is called from the print_question method.
694 * @param object $question The question to be rendered. Question type
695 * specific information is included. The name
696 * prefix for any named elements is in ->name_prefix.
697 * @param object $state The state to render the question in. The grading
698 * information is in ->grade, ->raw_grade and
699 * ->penalty. The current responses are in
700 * ->responses. This is an associative array (or the
701 * empty string or null in the case of no responses
702 * submitted). The last graded state is in
703 * ->last_graded (hence the most recently graded
704 * responses are in ->last_graded->responses). The
705 * question type specific information is also
706 * included.
707 * The state is passed by reference because some adaptive
708 * questions may want to update it during rendering
709 * @param object $quiz The quiz to which the question belongs. The
710 * question might be rendered differently
711 * depending on the quiz settings.
712 * @param object $options An object describing the rendering options.
714 function print_question_formulation_and_controls(&$question, &$state, $quiz, $options) {
715 /* This default implementation prints an error and must be overridden
716 by all question type implementations, unless the default implementation
717 of print_question has been overridden. */
719 notify('Error: Question formulation and input controls has not'
720 .' been implemented for question type '.$this->name());
724 * Prints the submit button(s) for the question in the given state
726 * This function prints the submit button(s) for the question in the
727 * given state. The name of any button created will be prefixed with the
728 * unique prefix for the question in $question->name_prefix. The suffix
729 * 'mark' is reserved for the single question mark button and the suffix
730 * 'validate' is reserved for the single question validate button (for
731 * question types which support it). Other suffixes will result in a response
732 * of that name in $state->responses which the printing and grading methods
733 * can then use.
734 * @param object $question The question for which the submit button(s) are to
735 * be rendered. Question type specific information is
736 * included. The name prefix for any
737 * named elements is in ->name_prefix.
738 * @param object $state The state to render the buttons for. The
739 * question type specific information is also
740 * included.
741 * @param object $quiz The quiz to which the question belongs. The
742 * choice of buttons may depend on the quiz
743 * settings.
744 * @param object $options An object describing the rendering options.
746 function print_question_submit_buttons(&$question, &$state, $quiz, $options) {
747 /* The default implementation should be suitable for most question
748 types. It prints a mark button in the case where individual marking is
749 allowed in the quiz. */
751 if($quiz->optionflags & QUIZ_ADAPTIVE) {
752 echo '<input type="submit" name="';
753 echo $question->name_prefix;
754 echo 'mark" value="';
755 print_string('mark', 'quiz');
756 echo '" />';
762 * Return a summary of the student response
764 * This function returns a short string of no more than a given length that
765 * summarizes the student's response in the given $state. This is used for
766 * example in the response history table
767 * @return string The summary of the student response
768 * @param object $state The state whose responses are to be summarized
769 * @param int $length The maximum length of the returned string
771 function response_summary($state, $length=80) {
772 // This should almost certainly be overridden
773 return substr($state->answer, 0, $length);
777 * Renders the question for printing and returns the LaTeX source produced
779 * This function should render the question suitable for a printed problem
780 * or solution sheet in LaTeX and return the rendered output.
781 * @return string The LaTeX output.
782 * @param object $question The question to be rendered. Question type
783 * specific information is included.
784 * @param object $state The state to render the question in. The
785 * question type specific information is also
786 * included.
787 * @param object $quiz The quiz to which the question belongs. The
788 * question will likely be rendered differently
789 * depending on the quiz settings.
790 * @param string $type Indicates if the question or the solution is to be
791 * rendered with the values 'question' and
792 * 'solution'.
794 function get_texsource(&$question, &$state, $quiz, $type) {
795 // The default implementation simply returns a string stating that
796 // the question is only available online.
798 return get_string('onlineonly', 'texsheet');
802 * Compares two question states for equivalence of the student's responses
804 * The responses for the two states must be examined to see if they represent
805 * equivalent answers to the question by the student. This method will be
806 * invoked for each of the previous states of the question before grading
807 * occurs. If the student is found to have already attempted the question
808 * with equivalent responses then the attempt at the question is ignored;
809 * grading does not occur and the state does not change. Thus they are not
810 * penalized for this case.
811 * @return boolean
812 * @param object $question The question for which the states are to be
813 * compared. Question type specific information is
814 * included.
815 * @param object $state The state of the question. The responses are in
816 * ->responses.
817 * @param object $teststate The state whose responses are to be
818 * compared. The state will be of the same age or
819 * older than $state.
821 function compare_responses(&$question, $state, $teststate) {
822 // The default implementation performs a comparison of the response
823 // arrays. The ordering of the arrays does not matter.
824 // Question types may wish to override this (eg. to ignore trailing
825 // white space or to make "7.0" and "7" compare equal).
826 return $state->responses == $teststate->responses;
830 * Performs response processing and grading
832 * This function performs response processing and grading and updates
833 * the state accordingly.
834 * @return boolean Indicates success or failure.
835 * @param object $question The question to be graded. Question type
836 * specific information is included.
837 * @param object $state The state of the question to grade. The current
838 * responses are in ->responses. The last graded state
839 * is in ->last_graded (hence the most recently graded
840 * responses are in ->last_graded->responses). The
841 * question type specific information is also
842 * included. The ->raw_grade and ->penalty fields
843 * must be updated. The method is able to
844 * close the question session (preventing any further
845 * attempts at this question) by setting
846 * $state->event to QUIZ_EVENTCLOSE.
847 * @param object $quiz The quiz to which the question belongs. The
848 * question might be graded differently depending on
849 * the quiz settings.
851 function grade_responses(&$question, &$state, $quiz) {
852 /* The default implementation uses the comparison method to check if
853 the responses given are equivalent to the responses for each answer
854 in turn and sets the marks and penalty accordingly. This works for the
855 most simple question types. */
857 $teststate = clone($state);
858 $teststate->raw_grade = 0;
859 foreach($question->options->answers as $answer) {
860 $teststate->responses[''] = $answer->answer;
862 if($this->compare_responses($question, $state, $teststate)) {
863 $state->raw_grade = min(max((float) $answer->fraction,
864 0.0), 1.0) * $question->maxgrade;
865 break;
868 if (empty($state->raw_grade)) {
869 $state->raw_grade = 0.0;
871 // Only allow one attempt at the question
872 $state->penalty = 1;
874 return true;
879 * Includes configuration settings for the question type on the quiz admin
880 * page
882 * Returns an array of objects describing the options for the question type
883 * to be included on the quiz module admin page.
884 * Configuration options can be included by setting the following fields in
885 * the object:
886 * ->name The name of the option within this question type.
887 * The full option name will be constructed as
888 * "quiz_{$this->name()}_$name", the human readable name
889 * will be displayed with get_string($name, 'quiz').
890 * ->code The code to display the form element, help button, etc.
891 * i.e. the content for the central table cell. Be sure
892 * to name the element "quiz_{$this->name()}_$name" and
893 * set the value to $CFG->{"quiz_{$this->name()}_$name"}.
894 * ->help Name of the string from the quiz module language file
895 * to be used for the help message in the third column of
896 * the table. An empty string (or the field not set)
897 * means to leave the box empty.
898 * Links to custom settings pages can be included by setting the following
899 * fields in the object:
900 * ->name The name of the link text string.
901 * get_string($name, 'quiz') will be called.
902 * ->link The filename part of the URL for the link. The full URL
903 * is contructed as
904 * "$CFG->wwwroot/mod/quiz/questiontypes/{$this->name()}/$link?sesskey=$sesskey"
905 * [but with the relavant calls to the s and rawurlencode
906 * functions] where $sesskey is the sesskey for the user.
907 * @return array Array of objects describing the configuration options to
908 * be included on the quiz module admin page.
910 function get_config_options() {
911 // No options by default
913 return false;
917 * Returns true if the editing wizard is finished, false otherwise. The
918 * default implementation returns true, which is suitable for all question-
919 * types that only use one editing form. This function is used in
920 * question.php to decide whether we can regrade any states of the edited
921 * question and redirect to edit.php.
923 * The dataset dependent question-type, which is extended by the calculated
924 * question-type, overwrites this method because it uses multiple pages (i.e.
925 * a wizard) to set up the question and associated datasets.
927 * @param object $form The data submitted by the previous page.
929 * @return boolean Whether the wizard's last page was submitted or not.
931 function finished_edit_wizard(&$form) {
932 //In the default case there is only one edit page.
933 return true;
936 function print_replacement_options($question, $course, $quizid='0') {
937 // This function is used near the end of the question edit forms in all question types
938 // It prints the table of quizzes in which the question is used
939 // containing checkboxes to allow the teacher to replace the old question version
941 // Disable until the versioning code has been fixed
942 return;
944 // no need to display replacement options if the question is new
945 if(empty($question->id)) {
946 return true;
949 // get quizzes using the question (using the question_instances table)
950 $quizlist = array();
951 if(!$instances = get_records('quiz_question_instances', 'question', $question->id)) {
952 $instances = array();
954 foreach($instances as $instance) {
955 $quizlist[$instance->quiz] = $instance->quiz;
957 $quizlist = implode(',', $quizlist);
958 if(empty($quizlist) or !$quizzes = get_records_list('quiz', 'id', $quizlist)) {
959 $quizzes = array();
962 // do the printing
963 if(count($quizzes) > 0) {
964 // print the table
965 $strquizname = get_string('modulename', 'quiz');
966 $strdoreplace = get_string('replace', 'quiz');
967 $straffectedstudents = get_string('affectedstudents', 'quiz', $course->students);
968 echo "<tr valign=\"top\">\n";
969 echo "<td align=\"right\"><b>".get_string("replacementoptions", "quiz").":</b></td>\n";
970 echo "<td align=\"left\">\n";
971 echo "<table cellpadding=\"5\" align=\"left\" class=\"generalbox\" width=\"100%\">\n";
972 echo "<tr>\n";
973 echo "<th align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\">$strquizname</th>\n";
974 echo "<th align=\"center\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\">$strdoreplace</th>\n";
975 echo "<th align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\">$straffectedstudents</th>\n";
976 echo "</tr>\n";
977 foreach($quizzes as $quiz) {
978 // work out whethere it should be checked by default
979 $checked = '';
980 if((int)$quizid === (int)$quiz->id
981 or empty($quiz->usercount)) {
982 $checked = "checked=\"checked\"";
985 // find how many different students have already attempted this quiz
986 $students = array();
987 if($attempts = get_records_select('quiz_attempts', "quiz = '$quiz->id' AND preview = '0'")) {
988 foreach($attempts as $attempt) {
989 if (record_exists('quiz_states', 'attempt', $attempt->id, 'question', $question->id, 'originalquestion', 0)) {
990 $students[$attempt->userid] = 1;
994 $studentcount = count($students);
996 $strstudents = $studentcount === 1 ? $course->student : $course->students;
997 echo "<tr>\n";
998 echo "<td align=\"left\" class=\"generaltablecell c0\">".format_string($quiz->name)."</td>\n";
999 echo "<td align=\"center\" class=\"generaltablecell c0\"><input name=\"q{$quiz->id}replace\" type=\"checkbox\" ".$checked." /></td>\n";
1000 echo "<td align=\"left\" class=\"generaltablecell c0\">".(($studentcount) ? $studentcount.' '.$strstudents : '-')."</td>\n";
1001 echo "</tr>\n";
1003 echo "</table>\n";
1005 echo "</td></tr>\n";
1008 function print_question_form_end($question, $submitscript='') {
1009 // This function is used at the end of the question edit forms in all question types
1010 // It prints the submit, copy, and cancel buttons and the standard hidden form fields
1011 global $USER;
1012 echo '<tr valign="top">
1013 <td colspan="2" align="center">
1014 <input type="submit" '.$submitscript.' value="'.get_string('savechanges').'" /> ';
1015 if ($question->id) {
1016 // Switched off until bug 3445 is fixed
1017 // echo '<input type="submit" name="makecopy" '.$submitscript.' value="'.get_string("makecopy", "quiz").'" /> ';
1019 echo '<input type="submit" name="cancel" value="'.get_string("cancel").'" />
1020 <input type="hidden" name="sesskey" value="'.$USER->sesskey.'" />
1021 <input type="hidden" name="id" value="'.$question->id.'" />
1022 <input type="hidden" name="qtype" value="'.$question->qtype.'" />';
1023 // The following hidden field indicates that the versioning code should be turned on, i.e.,
1024 // that old versions should be kept if necessary
1025 echo '<input type="hidden" name="versioning" value="on" />
1026 </td></tr>';
1031 /// QUIZ_QTYPES INITIATION //////////////////
1033 quiz_load_questiontypes();
1036 * Loads the questiontype.php file for each question type
1038 * These files in turn instantiate the corresponding question type class
1039 * and adds it to the $QUIZ_QTYPES array
1041 function quiz_load_questiontypes() {
1042 global $QUIZ_QTYPES;
1043 global $CFG;
1045 $qtypenames= get_list_of_plugins('mod/quiz/questiontypes');
1046 foreach($qtypenames as $qtypename) {
1047 // Instanciates all plug-in question types
1048 $qtypefilepath= "$CFG->dirroot/mod/quiz/questiontypes/$qtypename/questiontype.php";
1050 // echo "Loading $qtypename<br/>"; // Uncomment for debugging
1051 if (is_readable($qtypefilepath)) {
1052 require_once($qtypefilepath);
1060 //////////////////////////////////////////////////////////////////////////////////////
1061 /// Any other quiz functions go here. Each of them must have a name that
1062 /// starts with quiz_
1065 * Move all questions in $category1 to $category2
1067 * @return boolean indicate Success/Failure
1068 * @param $category1 the id of the category to move away from
1069 * @param $category2 the id of the category to move to
1071 function quiz_move_questions($category1, $category2) {
1072 global $CFG;
1073 return execute_sql("UPDATE {$CFG->prefix}quiz_questions
1074 SET category = '$category2'
1075 WHERE category = '$category1'",
1076 false);
1080 * Construct name prefixes for question form element names
1082 * Construct the name prefix that should be used for example in the
1083 * names of form elements created by questions for inclusion in the
1084 * quiz page. This is called by {@link quiz_get_question_options()}
1085 * to set $question->name_prefix.
1086 * This name prefix includes the question id which can be
1087 * extracted from it with {@link quiz_get_id_from_name_prefix()}.
1089 * @return string
1090 * @param integer $id The question id
1092 function quiz_make_name_prefix($id) {
1093 return 'resp' . $id . '_';
1097 * Extract question id from the prefix of form element names
1099 * @return integer The question id
1100 * @param string $name The name that contains a prefix that was
1101 * constructed with {@link quiz_make_name_prefix()}
1103 function quiz_get_id_from_name_prefix($name) {
1104 if (!preg_match('/^resp([0-9]+)_/', $name, $matches))
1105 return false;
1106 return (integer) $matches[1];
1110 * Updates the question objects in an array with the question type specific
1111 * information for each one by calling {@link get_question_options()}
1113 * The get_question_options method of the question type of each question in the
1114 * array is called to add the options field to the question object.
1115 * @return bool Indicates success or failure.
1116 * @param array $questions The array of question objects to be updated.
1118 function quiz_get_question_options(&$questions) {
1119 global $QUIZ_QTYPES;
1121 // get the keys of the input array
1122 $keys = array_keys($questions);
1123 // update each question object
1124 foreach ($keys as $i) {
1125 // set name prefix
1126 $questions[$i]->name_prefix = quiz_make_name_prefix($i);
1128 if (!$QUIZ_QTYPES[$questions[$i]->qtype]->get_question_options($questions[$i]))
1129 return false;
1131 return true;
1135 * Creates an object to represent a new attempt at a quiz
1137 * Creates an attempt object to represent an attempt at the quiz by the current
1138 * user starting at the current time. The ->id field is not set. The object is
1139 * NOT written to the database.
1140 * @return object The newly created attempt object.
1141 * @param object $quiz The quiz to create an attempt for.
1142 * @param integer $attemptnumber The sequence number for the attempt.
1144 function quiz_create_attempt($quiz, $attemptnumber) {
1145 global $USER;
1147 if (!$attemptnumber > 1 or !$quiz->attemptonlast or !$attempt = get_record('quiz_attempts', 'quiz', $quiz->id, 'userid', $USER->id, 'attempt', $attemptnumber-1)) {
1148 // we are not building on last attempt so create a new attempt
1149 $attempt->quiz = $quiz->id;
1150 $attempt->userid = $USER->id;
1151 $attempt->preview = 0;
1152 if ($quiz->shufflequestions) {
1153 $attempt->layout = quiz_repaginate($quiz->questions, $quiz->questionsperpage, true);
1154 } else {
1155 $attempt->layout = $quiz->questions;
1159 $timenow = time();
1160 $attempt->attempt = $attemptnumber;
1161 $attempt->sumgrades = 0.0;
1162 $attempt->timestart = $timenow;
1163 $attempt->timefinish = 0;
1164 $attempt->timemodified = $timenow;
1166 return $attempt;
1171 * Loads the most recent state of each question session from the database
1172 * or create new one.
1174 * For each question the most recent session state for the current attempt
1175 * is loaded from the quiz_states table and the question type specific data and
1176 * responses are added by calling {@link quiz_restore_state()} which in turn
1177 * calls {@link restore_session_and_responses()} for each question.
1178 * If no states exist for the question instance an empty state object is
1179 * created representing the start of a session and empty question
1180 * type specific information and responses are created by calling
1181 * {@link create_session_and_responses()}.
1182 * @todo Allow new attempt to be based on last attempt
1184 * @return array An array of state objects representing the most recent
1185 * states of the question sessions.
1186 * @param array $questions The questions for which sessions are to be restored or
1187 * created.
1188 * @param object $quiz The quiz to which the questions belong.
1189 * @param object $attempt The quiz attempt for which the question sessions are
1190 * to be restored or created.
1192 function quiz_restore_question_sessions(&$questions, $quiz, $attempt) {
1193 global $CFG, $QUIZ_QTYPES;
1195 // get the question ids
1196 $ids = array_keys($questions);
1197 $questionlist = implode(',', $ids);
1199 // The question field must be listed first so that it is used as the
1200 // array index in the array returned by get_records_sql
1201 $statefields = 'n.questionid as question, s.*, n.sumpenalty';
1202 // Load the newest states for the questions
1203 $sql = "SELECT $statefields".
1204 " FROM {$CFG->prefix}quiz_states s,".
1205 " {$CFG->prefix}quiz_newest_states n".
1206 " WHERE s.id = n.newest".
1207 " AND n.attemptid = '$attempt->id'".
1208 " AND n.questionid IN ($questionlist)";
1209 $states = get_records_sql($sql);
1211 // Load the newest graded states for the questions
1212 $sql = "SELECT $statefields".
1213 " FROM {$CFG->prefix}quiz_states s,".
1214 " {$CFG->prefix}quiz_newest_states n".
1215 " WHERE s.id = n.newgraded".
1216 " AND n.attemptid = '$attempt->id'".
1217 " AND n.questionid IN ($questionlist)";
1218 $gradedstates = get_records_sql($sql);
1220 // loop through all questions and set the last_graded states
1221 foreach ($ids as $i) {
1222 if (isset($states[$i])) {
1223 quiz_restore_state($questions[$i], $states[$i]);
1224 if (isset($gradedstates[$i])) {
1225 quiz_restore_state($questions[$i], $gradedstates[$i]);
1226 $states[$i]->last_graded = $gradedstates[$i];
1227 } else {
1228 $states[$i]->last_graded = clone($states[$i]);
1229 $states[$i]->last_graded->responses = array('' => '');
1231 } else {
1232 // Create a new state object
1233 if ($quiz->attemptonlast and $attempt->attempt > 1 and !$attempt->preview) {
1234 // build on states from last attempt
1235 if (!$lastattemptid = get_field('quiz_attempts', 'id', 'quiz', $attempt->quiz, 'userid', $attempt->userid, 'attempt', $attempt->attempt-1)) {
1236 error('Could not find previous attempt to build on');
1238 // Load the last graded state for the question
1239 $sql = "SELECT $statefields".
1240 " FROM {$CFG->prefix}quiz_states s,".
1241 " {$CFG->prefix}quiz_newest_states n".
1242 " WHERE s.id = n.newgraded".
1243 " AND n.attemptid = '$lastattemptid'".
1244 " AND n.questionid = '$i'";
1245 if (!$states[$i] = get_record_sql($sql)) {
1246 error('Could not find state for previous attempt to build on');
1248 quiz_restore_state($questions[$i], $states[$i]);
1249 $states[$i]->attempt = $attempt->id;
1250 $states[$i]->question = (int) $i;
1251 $states[$i]->seq_number = 0;
1252 $states[$i]->timestamp = $attempt->timestart;
1253 $states[$i]->event = ($attempt->timefinish) ? QUIZ_EVENTCLOSE : QUIZ_EVENTOPEN;
1254 $states[$i]->grade = '';
1255 $states[$i]->raw_grade = '';
1256 $states[$i]->penalty = '';
1257 $states[$i]->sumpenalty = '0.0';
1258 $states[$i]->changed = true;
1259 $states[$i]->last_graded = clone($states[$i]);
1260 $states[$i]->last_graded->responses = array('' => '');
1262 } else {
1263 // create a new empty state
1264 $states[$i] = new object;
1265 $states[$i]->attempt = $attempt->id;
1266 $states[$i]->question = (int) $i;
1267 $states[$i]->seq_number = 0;
1268 $states[$i]->timestamp = $attempt->timestart;
1269 $states[$i]->event = ($attempt->timefinish) ? QUIZ_EVENTCLOSE : QUIZ_EVENTOPEN;
1270 $states[$i]->grade = '';
1271 $states[$i]->raw_grade = '';
1272 $states[$i]->penalty = '';
1273 $states[$i]->sumpenalty = '0.0';
1274 $states[$i]->responses = array('' => '');
1275 // Prevent further changes to the session from incrementing the
1276 // sequence number
1277 $states[$i]->changed = true;
1279 // Create the empty question type specific information
1280 if (!$QUIZ_QTYPES[$questions[$i]->qtype]
1281 ->create_session_and_responses($questions[$i], $states[$i], $quiz, $attempt)) {
1282 return false;
1284 $states[$i]->last_graded = clone($states[$i]);
1288 return $states;
1293 * Creates the run-time fields for the states
1295 * Extends the state objects for a question by calling
1296 * {@link restore_session_and_responses()}
1297 * @return boolean Represents success or failure
1298 * @param array $questions The questions for which states are needed
1299 * @param array $states The states as loaded from the database, indexed
1300 * by question id
1302 function quiz_restore_state(&$question, &$state) {
1303 global $QUIZ_QTYPES;
1305 // initialise response to the value in the answer field
1306 $state->responses = array('' => $state->answer);
1307 unset($state->answer);
1309 // Set the changed field to false; any code which changes the
1310 // question session must set this to true and must increment
1311 // ->seq_number. The quiz_save_question_session
1312 // function will save the new state object database if the field is
1313 // set to true.
1314 $state->changed = false;
1316 // Load the question type specific data
1317 return $QUIZ_QTYPES[$question->qtype]
1318 ->restore_session_and_responses($question, $state);
1323 * Saves the current state of the question session to the database
1325 * The state object representing the current state of the session for the
1326 * question is saved to the quiz_states table with ->responses[''] saved
1327 * to the answer field of the database table. The information in the
1328 * quiz_newest_states table is updated.
1329 * The question type specific data is then saved.
1330 * @return boolean Indicates success or failure.
1331 * @param object $question The question for which session is to be saved.
1332 * @param object $state The state information to be saved. In particular the
1333 * most recent responses are in ->responses. The object
1334 * is updated to hold the new ->id.
1336 function quiz_save_question_session(&$question, &$state) {
1337 global $QUIZ_QTYPES;
1338 // Check if the state has changed
1339 if (!$state->changed && isset($state->id)) {
1340 return true;
1342 // Set the legacy answer field
1343 $state->answer = isset($state->responses['']) ? $state->responses[''] : '';
1345 // Save the state
1346 if (isset($state->update)) {
1347 update_record('quiz_states', $state);
1348 } else {
1349 if (!$state->id = insert_record('quiz_states', $state)) {
1350 unset($state->id);
1351 unset($state->answer);
1352 return false;
1355 // this is the most recent state
1356 if (!record_exists('quiz_newest_states', 'attemptid',
1357 $state->attempt, 'questionid', $question->id)) {
1358 $new->attemptid = $state->attempt;
1359 $new->questionid = $question->id;
1360 $new->newest = $state->id;
1361 $new->sumpenalty = $state->sumpenalty;
1362 if (!insert_record('quiz_newest_states', $new)) {
1363 error('Could not insert entry in quiz_newest_states');
1365 } else {
1366 set_field('quiz_newest_states', 'newest', $state->id, 'attemptid',
1367 $state->attempt, 'questionid', $question->id);
1369 if (quiz_state_is_graded($state)) {
1370 // this is also the most recent graded state
1371 if ($newest = get_record('quiz_newest_states', 'attemptid',
1372 $state->attempt, 'questionid', $question->id)) {
1373 $newest->newgraded = $state->id;
1374 $newest->sumpenalty = $state->sumpenalty;
1375 update_record('quiz_newest_states', $newest);
1380 unset($state->answer);
1382 // Save the question type specific state information and responses
1383 if (!$QUIZ_QTYPES[$question->qtype]->save_session_and_responses(
1384 $question, $state)) {
1385 return false;
1387 // Reset the changed flag
1388 $state->changed = false;
1389 return true;
1393 * Determines whether a state has been graded by looking at the event field
1395 * @return boolean true if the state has been graded
1396 * @param object $state
1398 function quiz_state_is_graded($state) {
1399 return ($state->event == QUIZ_EVENTGRADE or $state->event == QUIZ_EVENTCLOSE);
1403 * Updates a state object for the next new state to record the fact that the
1404 * question session has changed
1406 * If the question session is not already marked as having changed (via the
1407 * ->changed field of the state object), then this is done, the sequence
1408 * number in ->seq_number is incremented and the timestamp in ->timestamp is
1409 * updated. This should be called before or after any code which changes the
1410 * question session.
1411 * @param object $state The state object representing the state of the session.
1413 function quiz_mark_session_change(&$state) {
1414 if (!$state->changed) {
1415 $state->changed = true;
1416 $state->seq_number++;
1417 $state->timestamp = time();
1423 * Extracts responses from submitted form
1425 * TODO: Finish documenting this
1426 * @return array array of action objects, indexed by question ids.
1427 * @param array $questions an array containing at least all questions that are used on the form
1428 * @param array $responses
1429 * @param integer $defaultevent
1431 function quiz_extract_responses($questions, $responses, $defaultevent) {
1433 $actions = array();
1434 foreach ($responses as $key => $response) {
1435 // Get the question id from the response name
1436 if (false !== ($quid = quiz_get_id_from_name_prefix($key))) {
1437 // check if this is a valid id
1438 if (!isset($questions[$quid])) {
1439 error('Form contained question that is not in questionids');
1442 // Remove the name prefix from the name
1443 $key = substr($key, strlen($questions[$quid]->name_prefix));
1444 if (false === $key) {
1445 $key = '';
1448 // Check for question validate and mark buttons & set events
1449 if ($key === 'validate') {
1450 $actions[$quid]->event = QUIZ_EVENTVALIDATE;
1451 } else if ($key === 'mark') {
1452 $actions[$quid]->event = QUIZ_EVENTGRADE;
1453 } else {
1454 $actions[$quid]->event = $defaultevent;
1457 // Update the state with the new response
1458 $actions[$quid]->responses[$key] = $response;
1461 return $actions;
1467 * For a given question in an attempt we walk the complete history of states
1468 * and recalculate the grades as we go along.
1470 * This is used when a question in an existing quiz is changed and old student
1471 * responses need to be marked with the new version of a question.
1473 * TODO: Finish documenting this
1474 * @return boolean Indicates success/failure
1475 * @param object $question A question object
1476 * @param object $attempt The attempt, in which the question needs to be regraded.
1477 * @param object $quiz Optional. The quiz object that the attempt corresponds to.
1478 * @param boolean $verbose Optional. Whether to print progress information or not.
1480 function quiz_regrade_question_in_attempt($question, $attempt, $quiz=false, $verbose=false) {
1481 if (!$quiz && !($quiz = get_record('quiz', 'id', $attempt->quiz))) {
1482 $verbose && notify("Regrading of quiz #{$attempt->quiz} failed; " .
1483 "Couldn't load quiz record from database!");
1484 return false;
1487 if ($states = get_records_select('quiz_states',
1488 "attempt = '{$attempt->id}' AND question = '{$question->id}'", 'seq_number ASC')) {
1489 $states = array_values($states);
1491 $attempt->sumgrades -= $states[count($states)-1]->grade;
1493 // Initialise the replaystate
1494 $state = clone($states[0]);
1495 quiz_restore_state($question, $state);
1496 $state->sumpenalty = 0.0;
1497 $state->raw_grade = 0;
1498 $state->grade = 0;
1499 $state->responses = array(''=>'');
1500 $state->event = QUIZ_EVENTOPEN;
1501 $replaystate = clone($state);
1502 $replaystate->last_graded = $state;
1504 $changed = 0;
1505 for($j = 0; $j < count($states); $j++) {
1506 quiz_restore_state($question, $states[$j]);
1507 $action = new stdClass;
1508 $action->responses = $states[$j]->responses;
1509 $action->timestamp = $states[$j]->timestamp;
1511 // Close the last state of a finished attempt
1512 if (((count($states) - 1) === $j) && ($attempt->timefinish > 0)) {
1513 $action->event = QUIZ_EVENTCLOSE;
1515 // Grade instead of closing, quiz_process_responses will then
1516 // work out whether to close it
1517 } else if (QUIZ_EVENTCLOSE == $states[$j]->event) {
1518 $action->event = QUIZ_EVENTGRADE;
1520 // By default take the event that was saved in the database
1521 } else {
1522 $action->event = $states[$j]->event;
1524 // Reprocess (regrade) responses
1525 if (!quiz_process_responses($question, $replaystate, $action, $quiz,
1526 $attempt)) {
1527 $verbose && notify("Couldn't regrade state #{$state->id}!");
1529 if ((float)$replaystate->raw_grade != (float)$states[$j]->raw_grade) {
1530 $changed++;
1533 $replaystate->id = $states[$j]->id;
1534 $replaystate->update = true;
1535 quiz_save_question_session($question, $replaystate);
1537 if ($verbose) {
1538 if ($changed) {
1539 link_to_popup_window ('/mod/quiz/reviewquestion.php?attempt='.$attempt->id.'&amp;question='.$question->id,
1540 'reviewquestion', ' #'.$attempt->id, 450, 550, get_string('reviewresponse', 'quiz'));
1541 update_record('quiz_attempts', $attempt);
1542 } else {
1543 echo ' #'.$attempt->id;
1545 echo "\n"; flush(); ob_flush();
1548 return true;
1550 return true;
1554 * Processes an array of student responses, grading and saving them as appropriate
1556 * @return boolean Indicates success/failure
1557 * @param object $question Full question object, passed by reference
1558 * @param object $state Full state object, passed by reference
1559 * @param object $action object with the fields ->responses which
1560 * is an array holding the student responses,
1561 * ->action which specifies the action, e.g., QUIZ_EVENTGRADE,
1562 * and ->timestamp which is a timestamp from when the responses
1563 * were submitted by the student.
1564 * @param object $quiz The quiz object
1565 * @param object $attempt The attempt is passed by reference so that
1566 * during grading its ->sumgrades field can be updated
1568 * @todo There is a variable $quiz->ignoredupresp which makes the function go through
1569 * all previous states when checking if a response is duplicated. There is no user
1570 * interface for this yet.
1572 function quiz_process_responses(&$question, &$state, $action, $quiz, &$attempt) {
1573 global $QUIZ_QTYPES;
1575 // make sure these are gone!
1576 unset($action->responses['mark'], $action->responses['validate']);
1578 // Check the question session is still open
1579 if (QUIZ_EVENTCLOSE == $state->event) {
1580 return true;
1582 // If $action->event is not set that implies saving
1583 if (! isset($action->event)) {
1584 $action->event = QUIZ_EVENTSAVE;
1586 // Check if we are grading the question; compare against last graded
1587 // responses, not last given responses in this case
1588 if (quiz_isgradingevent($action->event)) {
1589 $state->responses = $state->last_graded->responses;
1591 // Check for unchanged responses (exactly unchanged, not equivalent).
1592 // We also have to catch questions that the student has not yet attempted
1593 $sameresponses = (($state->responses == $action->responses) or
1594 ($state->responses == array(''=>'') && array_keys(array_count_values($action->responses))===array('')));
1596 if ($sameresponses and QUIZ_EVENTCLOSE != $action->event
1597 and QUIZ_EVENTVALIDATE != $action->event) {
1598 return true;
1601 // Roll back grading information to last graded state and set the new
1602 // responses
1603 $newstate = clone($state->last_graded);
1604 $newstate->responses = $action->responses;
1605 $newstate->seq_number = $state->seq_number + 1;
1606 $newstate->changed = true; // will assure that it gets saved to the database
1607 $newstate->last_graded = $state->last_graded;
1608 $newstate->timestamp = $action->timestamp;
1609 $state = $newstate;
1611 // Set the event to the action we will perform. The question type specific
1612 // grading code may override this by setting it to QUIZ_EVENTCLOSE if the
1613 // attempt at the question causes the session to close
1614 $state->event = $action->event;
1616 if (!quiz_isgradingevent($action->event)) {
1617 // Grade the response but don't update the overall grade
1618 $QUIZ_QTYPES[$question->qtype]->grade_responses(
1619 $question, $state, $quiz);
1620 // Force the event to save or validate (even if the grading caused the
1621 // state to close)
1622 $state->event = $action->event;
1624 } else if (QUIZ_EVENTGRADE == $action->event) {
1626 // Work out if the current responses (or equivalent responses) were
1627 // already given in
1628 // a. the last graded attempt
1629 // b. any other graded attempt
1630 if($QUIZ_QTYPES[$question->qtype]->compare_responses(
1631 $question, $state, $state->last_graded)) {
1632 $state->event = QUIZ_EVENTDUPLICATEGRADE;
1633 } else {
1634 if ($quiz->optionflags & QUIZ_IGNORE_DUPRESP) {
1635 /* Walk back through the previous graded states looking for
1636 one where the responses are equivalent to the current
1637 responses. If such a state is found, set the current grading
1638 details to those of that state and set the event to
1639 QUIZ_EVENTDUPLICATEGRADE */
1640 quiz_search_for_duplicate_responses($question, $state);
1642 // If we did not find a duplicate, perform grading
1643 if (QUIZ_EVENTDUPLICATEGRADE != $state->event) {
1644 // Decrease sumgrades by previous grade and then later add new grade
1645 $attempt->sumgrades -= (float)$state->last_graded->grade;
1647 $QUIZ_QTYPES[$question->qtype]->grade_responses(
1648 $question, $state, $quiz);
1649 // Calculate overall grade using correct penalty method
1650 quiz_apply_penalty_and_timelimit($question, $state, $attempt, $quiz);
1651 // Update the last graded state (don't simplify!)
1652 unset($state->last_graded);
1653 $state->last_graded = clone($state);
1654 unset($state->last_graded->changed);
1656 $attempt->sumgrades += (float)$state->last_graded->grade;
1659 } else if (QUIZ_EVENTCLOSE == $action->event) {
1660 // decrease sumgrades by previous grade and then later add new grade
1661 $attempt->sumgrades -= (float)$state->last_graded->grade;
1663 // Only mark if they haven't been marked already
1664 if (!$sameresponses) {
1665 $QUIZ_QTYPES[$question->qtype]->grade_responses(
1666 $question, $state, $quiz);
1667 // Calculate overall grade using correct penalty method
1668 quiz_apply_penalty_and_timelimit($question, $state, $attempt, $quiz);
1670 // Force the state to close (as the attempt is closing)
1671 $state->event = QUIZ_EVENTCLOSE;
1672 // If there is no valid grade, set it to zero
1673 if ('' === $state->grade) {
1674 $state->raw_grade = 0;
1675 $state->penalty = 0;
1676 $state->grade = 0;
1678 // Update the last graded state (don't simplify!)
1679 unset($state->last_graded);
1680 $state->last_graded = clone($state);
1681 unset($state->last_graded->changed);
1683 $attempt->sumgrades += (float)$state->last_graded->grade;
1685 $attempt->timemodified = $action->timestamp;
1686 return true;
1690 * Determine if event requires grading
1692 function quiz_isgradingevent($event) {
1693 return (QUIZ_EVENTGRADE == $event || QUIZ_EVENTCLOSE == $event);
1697 * Compare current responses to all previous graded responses
1699 * This is used by {@link quiz_process_responses()} to determine whether
1700 * to ignore the marking request for the current response. However this
1701 * check against all previous graded responses is only performed if
1702 * the QUIZ_IGNORE_DUPRESP bit in $quiz->optionflags is set
1703 * @return boolean Indicates if a state with duplicate responses was
1704 * found.
1705 * @param object $question
1706 * @param object $state
1708 function quiz_search_for_duplicate_responses(&$question, &$state) {
1709 // get all previously graded question states
1710 global $QUIZ_QTYPES;
1711 if (!$oldstates = get_records('quiz_question_states', "event = '" .
1712 QUIZ_EVENTGRADE . "' AND " . "question = '" . $question->id .
1713 "'", 'seq_number DESC')) {
1714 return false;
1716 foreach ($oldstates as $oldstate) {
1717 if ($QUIZ_QTYPES[$question->qtype]->restore_session_and_responses(
1718 $question, $oldstate)) {
1719 if(!$QUIZ_QTYPES[$question->qtype]->compare_responses(
1720 $question, $state, $oldstate)) {
1721 $state->event = QUIZ_EVENTDUPLICATEGRADE;
1722 break;
1726 return (QUIZ_EVENTDUPLICATEGRADE == $state->event);
1730 * Applies the penalty from the previous attempts to the raw grade for the current
1731 * attempt
1733 * The grade for the question in the current state is computed by subtracting the
1734 * penalty accumulated over the previous marked attempts at the question from the
1735 * raw grade. If the timestamp is more than 1 minute beyond the start of the attempt
1736 * the grade is set to zero. The ->grade field of the state object is modified to
1737 * reflect the new grade but is never allowed to decrease.
1738 * @param object $question The question for which the penalty is to be applied.
1739 * @param object $state The state for which the grade is to be set from the
1740 * raw grade and the cumulative penalty from the last
1741 * graded state. The ->grade field is updated by applying
1742 * the penalty scheme for the quiz to the ->raw_grade and
1743 * ->last_graded->penalty fields.
1744 * @param object $quiz The quiz to which the question belongs. The penalty
1745 * scheme to apply is given by the ->penaltyscheme field.
1747 function quiz_apply_penalty_and_timelimit(&$question, &$state, $attempt, $quiz) {
1748 // deal with penaly
1749 if ($quiz->penaltyscheme) {
1750 $state->grade = $state->raw_grade - $state->sumpenalty;
1751 $state->sumpenalty += (float) $state->penalty;
1752 } else {
1753 $state->grade = $state->raw_grade;
1756 // deal with timeimit
1757 if ($quiz->timelimit) {
1758 // We allow for 5% uncertainty in the following test
1759 if (($state->timestamp - $attempt->timestart) > ($quiz->timelimit * 63)) {
1760 $state->grade = 0;
1764 // deal with quiz closing time
1765 if ($state->timestamp > ($quiz->timeclose + 60)) { // allowing 1 minute lateness
1766 $state->grade = 0;
1769 // Ensure that the grade does not go down
1770 $state->grade = max($state->grade, $state->last_graded->grade);
1774 function quiz_print_comment($text) {
1775 echo "<span class=\"feedbacktext\">&nbsp;".format_text($text, true, false)."</span>";
1778 function quiz_print_correctanswer($text) {
1779 echo "<p align=\"right\"><span class=\"highlight\">$text</span></p>";
1783 * Print the icon for the question type
1785 * @param object $question The question object for which the icon is required
1786 * @param boolean $editlink If true then the icon is a link to the question
1787 * edit page.
1788 * @param boolean $return If true the functions returns the link as a string
1790 function quiz_print_question_icon($question, $editlink=true, $return = false) {
1791 // returns a question icon
1793 global $QUIZ_QUESTION_TYPE;
1794 global $QUIZ_QTYPES;
1796 $html = '<img border="0" height="16" width="16" src="questiontypes/'.
1797 $QUIZ_QTYPES[$question->qtype]->name().'/icon.gif" alt="'.
1798 get_string($QUIZ_QTYPES[$question->qtype]->name(), 'quiz').'" />';
1800 if ($editlink) {
1801 $html = "<a href=\"question.php?id=$question->id\" title=\""
1802 .$QUIZ_QTYPES[$question->qtype]->name()."\">".
1803 $html."</a>\n";
1805 if ($return) {
1806 return $html;
1807 } else {
1808 echo $html;
1812 // ULPGc ecastro
1813 function quiz_get_question_review($quiz, $question) {
1814 // returns a question icon
1815 $qnum = $question->id;
1816 $strpreview = get_string('previewquestion', 'quiz');
1817 $context = $quiz->id ? '&amp;contextquiz='.$quiz->id : '';
1818 $quiz_id = $quiz->id ? '&amp;quizid=' . $quiz->id : '';
1819 return "<a title=\"$strpreview\" href=\"javascript:void();\" onClick=\"openpopup('/mod/quiz/preview.php?id=$qnum$quiz_id','$strpreview','scrollbars=yes,resizable=yes,width=700,height=480', false)\">
1820 <img src=\"../../pix/t/preview.gif\" border=\"0\" alt=\"$strpreview\" /></a>";
1828 * Print the question image if there is one
1830 * @param integer $quizid The id of the quiz
1831 * @param object $question The question object
1833 function quiz_print_possible_question_image($quizid, $question) {
1835 global $CFG;
1837 if ($quizid == '') {
1838 $quizid = '0';
1841 if ($question->image) {
1842 echo '<img border="0" src="';
1844 if (substr(strtolower($question->image), 0, 7) == 'http://') {
1845 echo $question->image;
1847 } else if ($CFG->slasharguments) { // Use this method if possible for better caching
1848 echo "$CFG->wwwroot/mod/quiz/quizfile.php/$quizid/$question->id/$question->image";
1850 } else {
1851 echo "$CFG->wwwroot/mod/quiz/quizfile.php?file=/$quizid/$question->id/$question->image";
1853 echo '" alt="" />';
1859 * Returns a comma separated list of question ids for the current page
1861 * @return string Comma separated list of question ids
1862 * @param string $layout The string representing the quiz layout. Each page is represented as a
1863 * comma separated list of question ids and 0 indicating page breaks.
1864 * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
1865 * @param integer $page The number of the current page.
1867 function quiz_questions_on_page($layout, $page) {
1868 $pages = explode(',0', $layout);
1869 return trim($pages[$page], ',');
1873 * Returns a comma separated list of question ids for the quiz
1875 * @return string Comma separated list of question ids
1876 * @param string $layout The string representing the quiz layout. Each page is represented as a
1877 * comma separated list of question ids and 0 indicating page breaks.
1878 * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
1880 function quiz_questions_in_quiz($layout) {
1881 return str_replace(',0', '', $layout);
1885 * Returns the number of pages in the quiz layout
1887 * @return integer Comma separated list of question ids
1888 * @param string $layout The string representing the quiz layout.
1890 function quiz_number_of_pages($layout) {
1891 return substr_count($layout, ',0');
1895 * Returns the first question number for the current quiz page
1897 * @return integer The number of the first question
1898 * @param string $quizlayout The string representing the layout for the whole quiz
1899 * @param string $pagelayout The string representing the layout for the current page
1901 function quiz_first_questionnumber($quizlayout, $pagelayout) {
1902 // this works by finding all the questions from the quizlayout that
1903 // come before the current page and then adding up their lengths.
1904 global $CFG;
1905 $start = strpos($quizlayout, $pagelayout)-3;
1906 if ($start > 0) {
1907 $prevlist = substr($quizlayout, 0, $start);
1908 return get_field_sql("SELECT sum(length)+1 FROM {$CFG->prefix}quiz_questions
1909 WHERE id IN ($prevlist)");
1910 } else {
1911 return 1;
1916 * Re-paginates the quiz layout
1918 * @return string The new layout string
1919 * @param string $layout The string representing the quiz layout.
1920 * @param integer $perpage The number of questions per page
1921 * @param boolean $shuffle Should the questions be reordered randomly?
1923 function quiz_repaginate($layout, $perpage, $shuffle=false) {
1924 $layout = str_replace(',0', '', $layout); // remove existing page breaks
1925 $questions = explode(',', $layout);
1926 if ($shuffle) {
1927 srand((float)microtime() * 1000000); // for php < 4.2
1928 shuffle($questions);
1930 $i = 1;
1931 $layout = '';
1932 foreach ($questions as $question) {
1933 if ($perpage and $i > $perpage) {
1934 $layout .= '0,';
1935 $i = 1;
1937 $layout .= $question.',';
1938 $i++;
1940 return $layout.'0';
1944 * Print navigation panel for quiz attempt and review pages
1946 * @param integer $page The number of the current page (counting from 0).
1947 * @param integer $pages The total number of pages.
1949 function quiz_print_navigation_panel($page, $pages) {
1950 //$page++;
1951 echo '<div class="pagingbar">';
1952 echo '<span class="title">' . get_string('page') . ':</span>';
1953 if ($page > 0) {
1954 // Print previous link
1955 $strprev = get_string('previous');
1956 echo '<a href="javascript:navigate(' . ($page - 1) . ');" title="'
1957 . $strprev . '">(' . $strprev . ')</a>';
1959 for ($i = 0; $i < $pages; $i++) {
1960 if ($i == $page) {
1961 echo '<span class="thispage">'.($i+1).'</span>';
1962 } else {
1963 echo '<a href="javascript:navigate(' . ($i) . ');">'.($i+1).'</a>';
1967 if ($page < $pages - 1) {
1968 // Print next link
1969 $strnext = get_string('next');
1970 echo '<a href="javascript:navigate(' . ($page + 1) . ');" title="'
1971 . $strnext . '">(' . $strnext . ')</a>';
1973 echo '</div>';
1977 * Prints a question for a quiz page
1979 * Simply calls the question type specific print_question() method.
1981 function quiz_print_quiz_question(&$question, &$state, $number, $quiz, $options=null) {
1982 global $QUIZ_QTYPES;
1984 $QUIZ_QTYPES[$question->qtype]->print_question($question, $state, $number,
1985 $quiz, $options);
1989 * Gets all teacher stored answers for a given question
1991 * Simply calls the question type specific get_all_responses() method.
1993 // ULPGC ecastro
1994 function quiz_get_question_responses($question, $state) {
1995 global $QUIZ_QTYPES;
1996 $r = $QUIZ_QTYPES[$question->qtype]->get_all_responses($question, $state);
1997 return $r;
2002 * Gets the response given by the user in a particular attempt
2004 * Simply calls the question type specific get_actual_response() method.
2006 // ULPGC ecastro
2007 function quiz_get_question_actual_response($question, $state) {
2008 global $QUIZ_QTYPES;
2010 $r = $QUIZ_QTYPES[$question->qtype]->get_actual_response($question, $state);
2011 return $r;
2015 * Gets the response given by the user in a particular attempt
2017 * Simply calls the question type specific get_actual_response() method.
2019 // ULPGc ecastro
2020 function quiz_get_question_fraction_grade($question, $state) {
2021 global $QUIZ_QTYPES;
2023 $r = $QUIZ_QTYPES[$question->qtype]->get_fractional_grade($question, $state);
2024 return $r;
2029 * Gets the default category in a course
2031 * It returns the first category with no parent category. If no categories
2032 * exist yet then one is created.
2033 * @return object The default category
2034 * @param integer $courseid The id of the course whose default category is wanted
2036 function quiz_get_default_category($courseid) {
2037 /// Returns the current category
2039 if ($categories = get_records_select("quiz_categories", "course = '$courseid' AND parent = '0'", "id")) {
2040 foreach ($categories as $category) {
2041 return $category; // Return the first one (lowest id)
2045 // Otherwise, we need to make one
2046 $category->name = get_string("default", "quiz");
2047 $category->info = get_string("defaultinfo", "quiz");
2048 $category->course = $courseid;
2049 $category->parent = 0;
2050 $category->sortorder = QUIZ_CATEGORIES_SORTORDER;
2051 $category->publish = 0;
2052 $category->stamp = make_unique_id_code();
2054 if (!$category->id = insert_record("quiz_categories", $category)) {
2055 notify("Error creating a default category!");
2056 return false;
2058 return $category;
2061 function quiz_get_category_menu($courseid, $published=false) {
2062 /// Returns the list of categories
2063 $publish = "";
2064 if ($published) {
2065 $publish = "OR publish = '1'";
2068 if (!isadmin()) {
2069 $categories = get_records_select("quiz_categories", "course = '$courseid' $publish", 'parent, sortorder, name ASC');
2070 } else {
2071 $categories = get_records_select("quiz_categories", '', 'parent, sortorder, name ASC');
2073 if (!$categories) {
2074 return false;
2076 $categories = add_indented_names($categories);
2078 foreach ($categories as $category) {
2079 if ($catcourse = get_record("course", "id", $category->course)) {
2080 if ($category->publish && ($category->course != $courseid)) {
2081 $category->indentedname .= " ($catcourse->shortname)";
2083 $catmenu[$category->id] = $category->indentedname;
2086 return $catmenu;
2089 function sort_categories_by_tree(&$categories, $id = 0, $level = 1) {
2090 // returns the categories with their names ordered following parent-child relationships
2091 // finally it tries to return pending categories (those being orphaned, whose parent is
2092 // incorrect) to avoid missing any category from original array.
2093 $children = array();
2094 $keys = array_keys($categories);
2096 foreach ($keys as $key) {
2097 if (!isset($categories[$key]->processed) && $categories[$key]->parent == $id) {
2098 $children[$key] = $categories[$key];
2099 $categories[$key]->processed = true;
2100 $children = $children + sort_categories_by_tree($categories, $children[$key]->id, $level+1);
2103 //If level = 1, we have finished, try to look for non processed categories (bad parent) and sort them too
2104 if ($level == 1) {
2105 foreach ($keys as $key) {
2106 //If not processed and it's a good candidate to start (because its parent doesn't exist in the course)
2107 if (!isset($categories[$key]->processed) && !record_exists('quiz_categories', 'course', $categories[$key]->course, 'id', $categories[$key]->parent)) {
2108 $children[$key] = $categories[$key];
2109 $categories[$key]->processed = true;
2110 $children = $children + sort_categories_by_tree($categories, $children[$key]->id, $level+1);
2114 return $children;
2117 function add_indented_names(&$categories, $id = 0, $indent = 0) {
2118 // returns the categories with their names indented to show parent-child relationships
2119 $fillstr = '&nbsp;&nbsp;&nbsp;';
2120 $fill = str_repeat($fillstr, $indent);
2121 $children = array();
2122 $keys = array_keys($categories);
2124 foreach ($keys as $key) {
2125 if (!isset($categories[$key]->processed) && $categories[$key]->parent == $id) {
2126 $children[$key] = $categories[$key];
2127 $children[$key]->indentedname = $fill . $children[$key]->name;
2128 $categories[$key]->processed = true;
2129 $children = $children + add_indented_names($categories, $children[$key]->id, $indent + 1);
2132 return $children;
2136 * Displays a select menu of categories with appended course names
2138 * Optionaly non editable categories may be excluded.
2139 * @author Howard Miller June '04
2141 function quiz_category_select_menu($courseid,$published=false,$only_editable=false,$selected="") {
2143 // get sql fragment for published
2144 $publishsql="";
2145 if ($published) {
2146 $publishsql = "or publish=1";
2149 $categories = get_records_select("quiz_categories","course=$courseid $publishsql", 'parent, sortorder, name ASC');
2151 $categories = add_indented_names($categories);
2153 echo "<select name=\"category\">\n";
2154 foreach ($categories as $category) {
2155 $cid = $category->id;
2156 $cname = quiz_get_category_coursename($category, $courseid);
2157 $seltxt = "";
2158 if ($cid==$selected) {
2159 $seltxt = "selected=\"selected\"";
2161 if ((!$only_editable) || isteacheredit($category->course)) {
2162 echo " <option value=\"$cid\" $seltxt>$cname</option>\n";
2165 echo "</select>\n";
2168 function quiz_get_category_coursename($category, $courseid = 0) {
2169 /// if the category is not from this course and is published , adds on the course
2170 /// name
2171 $cname = (isset($category->indentedname)) ? $category->indentedname : $category->name;
2172 if ($category->course != $courseid && $category->publish) {
2173 if ($catcourse=get_record("course","id",$category->course)) {
2174 $cname .= " ($catcourse->shortname) ";
2177 return $cname;
2181 * Creates an array of maximum grades for a quiz
2183 * The grades are extracted from the quiz_question_instances table.
2184 * @return array Array of grades indexed by question id
2185 * These are the maximum possible grades that
2186 * students can achieve for each of the questions
2187 * @param integer $quiz The quiz object
2189 function quiz_get_all_question_grades($quiz) {
2190 global $CFG;
2192 $questionlist = quiz_questions_in_quiz($quiz->questions);
2193 if (empty($questionlist)) {
2194 return array();
2197 $instances = get_records_sql("SELECT question,grade,id
2198 FROM {$CFG->prefix}quiz_question_instances
2199 WHERE quiz = '$quiz->id'" .
2200 (is_null($questionlist) ? '' :
2201 "AND question IN ($questionlist)"));
2203 $list = explode(",", $questionlist);
2204 $grades = array();
2206 foreach ($list as $qid) {
2207 if (isset($instances[$qid])) {
2208 $grades[$qid] = $instances[$qid]->grade;
2209 } else {
2210 $grades[$qid] = 1;
2213 return $grades;
2216 function quiz_get_user_attempt_unfinished($quizid, $userid) {
2217 // Returns an object containing an unfinished attempt (if there is one)
2218 return get_record("quiz_attempts", "quiz", $quizid, "userid", $userid, "timefinish", 0);
2221 function quiz_get_user_attempts($quizid, $userid) {
2222 // Returns a list of all attempts by a user
2223 return get_records_select("quiz_attempts", "quiz = '$quizid' AND userid = '$userid' AND timefinish > 0",
2224 "attempt ASC");
2228 function quiz_get_best_grade($quiz, $userid) {
2229 /// Get the best current grade for a particular user in a quiz
2230 if (!$grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) {
2231 return NULL;
2234 return (round($grade->grade,$quiz->decimalpoints));
2238 * Save the overall grade for a user at a quiz in the quiz_grades table
2240 * @return boolean Indicates success or failure.
2241 * @param object $quiz The quiz for which the best grade is to be calculated
2242 * and then saved.
2243 * @param integer $userid The id of the user to save the best grade for. Can be
2244 * null in which case the current user is assumed.
2246 function quiz_save_best_grade($quiz, $userid=null) {
2247 global $USER;
2249 // Assume the current user if $userid is null
2250 if (is_null($userid)) {
2251 $userid = $USER->id;
2254 // Get all the attempts made by the user
2255 if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) {
2256 notify('Could not find any user attempts');
2257 return false;
2260 // Calculate the best grade
2261 $bestgrade = quiz_calculate_best_grade($quiz, $attempts);
2262 $bestgrade = (($bestgrade / $quiz->sumgrades) * $quiz->grade);
2263 $bestgrade = round($bestgrade, $quiz->decimalpoints);
2265 // Save the best grade in the database
2266 if ($grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid',
2267 $userid)) {
2268 $grade->grade = $bestgrade;
2269 $grade->timemodified = time();
2270 if (!update_record('quiz_grades', $grade)) {
2271 notify('Could not update best grade');
2272 return false;
2274 } else {
2275 $grade->quiz = $quiz->id;
2276 $grade->userid = $userid;
2277 $grade->grade = $bestgrade;
2278 $grade->timemodified = time();
2279 if (!insert_record('quiz_grades', $grade)) {
2280 notify('Could not insert new best grade');
2281 return false;
2284 return true;
2288 * Calculate the overall grade for a quiz given a number of attempts by a particular user.
2290 * @return float The overall grade
2291 * @param object $quiz The quiz for which the best grade is to be calculated
2292 * @param array $attempts An array of all the attempts of the user at the quiz
2294 function quiz_calculate_best_grade($quiz, $attempts) {
2296 switch ($quiz->grademethod) {
2298 case QUIZ_ATTEMPTFIRST:
2299 foreach ($attempts as $attempt) {
2300 return $attempt->sumgrades;
2302 break;
2304 case QUIZ_ATTEMPTLAST:
2305 foreach ($attempts as $attempt) {
2306 $final = $attempt->sumgrades;
2308 return $final;
2310 case QUIZ_GRADEAVERAGE:
2311 $sum = 0;
2312 $count = 0;
2313 foreach ($attempts as $attempt) {
2314 $sum += $attempt->sumgrades;
2315 $count++;
2317 return (float)$sum/$count;
2319 default:
2320 case QUIZ_GRADEHIGHEST:
2321 $max = 0;
2322 foreach ($attempts as $attempt) {
2323 if ($attempt->sumgrades > $max) {
2324 $max = $attempt->sumgrades;
2327 return $max;
2332 * Return the attempt with the best grade for a quiz
2334 * Which attempt is the best depends on $quiz->grademethod. If the grade
2335 * method is GRADEAVERAGE then this function simply returns the last attempt.
2336 * @return object The attempt with the best grade
2337 * @param object $quiz The quiz for which the best grade is to be calculated
2338 * @param array $attempts An array of all the attempts of the user at the quiz
2340 function quiz_calculate_best_attempt($quiz, $attempts) {
2342 switch ($quiz->grademethod) {
2344 case QUIZ_ATTEMPTFIRST:
2345 foreach ($attempts as $attempt) {
2346 return $attempt;
2348 break;
2350 case QUIZ_GRADEAVERAGE: // need to do something with it :-)
2351 case QUIZ_ATTEMPTLAST:
2352 foreach ($attempts as $attempt) {
2353 $final = $attempt;
2355 return $final;
2357 default:
2358 case QUIZ_GRADEHIGHEST:
2359 $max = -1;
2360 foreach ($attempts as $attempt) {
2361 if ($attempt->sumgrades > $max) {
2362 $max = $attempt->sumgrades;
2363 $maxattempt = $attempt;
2366 return $maxattempt;
2371 function quiz_extract_correctanswers($answers, $nameprefix) {
2372 /// Convenience function that is used by some single-response
2373 /// question-types for determining correct answers.
2375 $bestanswerfraction = 0.0;
2376 $correctanswers = array();
2377 foreach ($answers as $answer) {
2378 if ($answer->fraction > $bestanswerfraction) {
2379 $correctanswers = array($nameprefix.$answer->id => $answer);
2380 $bestanswerfraction = $answer->fraction;
2381 } else if ($answer->fraction == $bestanswerfraction) {
2382 $correctanswers[$nameprefix.$answer->id] = $answer;
2385 return $correctanswers;
2388 // this function creates default export filename
2389 function default_export_filename($course,$category) {
2390 //Take off some characters in the filename !!
2391 $takeoff = array(" ", ":", "/", "\\", "|");
2392 $export_word = str_replace($takeoff,"_",strtolower(get_string("exportfilename","quiz")));
2393 //If non-translated, use "export"
2394 if (substr($export_word,0,1) == "[") {
2395 $export_word= "export";
2398 //Calculate the date format string
2399 $export_date_format = str_replace(" ","_",get_string("exportnameformat","quiz"));
2400 //If non-translated, use "%Y%m%d-%H%M"
2401 if (substr($export_date_format,0,1) == "[") {
2402 $export_date_format = "%%Y%%m%%d-%%H%%M";
2405 //Calculate the shortname
2406 $export_shortname = clean_filename($course->shortname);
2407 if (empty($export_shortname) or $export_shortname == '_' ) {
2408 $export_shortname = $course->id;
2411 //Calculate the category name
2412 $export_categoryname = clean_filename($category->name);
2414 //Calculate the final export filename
2415 //The export word
2416 $export_name = $export_word."-";
2417 //The shortname
2418 $export_name .= strtolower($export_shortname)."-";
2419 //The category name
2420 $export_name .= strtolower($export_categoryname)."-";
2421 //The date format
2422 $export_name .= userdate(time(),$export_date_format,99,false);
2423 //The extension - no extension, supplied by format
2424 // $export_name .= ".txt";
2426 return $export_name;
2430 * Function to read all questions for category into big array
2432 * @param int $category category number
2433 * @param bool @noparent if true only questions with NO parent will be selected
2434 * @author added by Howard Miller June 2004
2436 function get_questions_category( $category, $noparent=false ) {
2438 global $QUIZ_QTYPES;
2440 // questions will be added to an array
2441 $qresults = array();
2443 // build sql bit for $noparent
2444 $npsql = '';
2445 if ($noparent) {
2446 $npsql = " and parent='0' ";
2449 // get the list of questions for the category
2450 if ($questions = get_records_select("quiz_questions","category={$category->id} $npsql", "qtype, name ASC")) {
2452 // iterate through questions, getting stuff we need
2453 foreach($questions as $question) {
2454 $questiontype = $QUIZ_QTYPES[$question->qtype];
2455 $questiontype->get_question_options( $question );
2456 $qresults[] = $question;
2460 return $qresults;
2464 * Returns a comma separated list of ids of the category and all subcategories
2466 function quiz_categorylist($categoryid) {
2467 // returns a comma separated list of ids of the category and all subcategories
2468 $categorylist = $categoryid;
2469 if ($subcategories = get_records('quiz_categories', 'parent', $categoryid, 'sortorder ASC', 'id, id')) {
2470 foreach ($subcategories as $subcategory) {
2471 $categorylist .= ','. quiz_categorylist($subcategory->id);
2474 return $categorylist;
2479 * Array of names of quizzes a question appears in
2481 * @return array Array of quiz names
2482 * @param integer Question id
2484 function quizzes_question_used($id) {
2486 $quizlist = array();
2487 if ($instances = get_records('quiz_question_instances', 'question', $id)) {
2488 foreach($instances as $instance) {
2489 $quizlist[$instance->quiz] = get_field('quiz', 'name', 'id', $instance->quiz);
2493 return $quizlist;
2497 * Array of names of quizzes a category (and optionally its childs) appears in
2499 * @return array Array of quiz names (with quiz->id as array keys)
2500 * @param integer Quiz category id
2501 * @param boolean Examine category childs recursively
2503 function quizzes_category_used($id, $recursive = false) {
2505 $quizlist = array();
2507 //Look for each question in the category
2508 if ($questions = get_records('quiz_questions', 'category', $id)) {
2509 foreach ($questions as $question) {
2510 $qlist = quizzes_question_used($question->id);
2511 $quizlist = $quizlist + $qlist;
2515 //Look under child categories recursively
2516 if ($recursive) {
2517 if ($childs = get_records('quiz_categories', 'parent', $id)) {
2518 foreach ($childs as $child) {
2519 $quizlist = $quizlist + quizzes_category_used($child->id, $recursive);
2524 return $quizlist;
2528 * Parse field names used for the replace options on question edit forms
2530 function quiz_parse_fieldname($name, $nameprefix='question') {
2531 $reg = array();
2532 if (preg_match("/$nameprefix(\\d+)(\w+)/", $name, $reg)) {
2533 return array('mode' => $reg[2], 'id' => (int)$reg[1]);
2534 } else {
2535 return false;
2541 * Determine render options
2543 function quiz_get_renderoptions($quiz, $state) {
2544 // Show the question in readonly (review) mode if the quiz is in
2545 // the closed state
2546 $options->readonly = QUIZ_EVENTCLOSE === $state->event;
2548 // Show feedback once the question has been graded (if allowed by the quiz)
2549 $options->feedback = ('' !== $state->grade) && ($quiz->review & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
2551 // Show validation only after a validation event
2552 $options->validation = QUIZ_EVENTVALIDATE === $state->event;
2554 // Show correct responses in readonly mode if the quiz allows it
2555 $options->correct_responses = $options->readonly && ($quiz->review & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
2557 // Always show responses and scores
2558 $options->responses = true;
2559 $options->scores = true;
2561 return $options;
2566 * Determine review options
2568 function quiz_get_reviewoptions($quiz, $attempt, $isteacher=false) {
2569 $options->readonly = true;
2570 if ($isteacher and !$attempt->preview) {
2571 // The teacher should be shown everything except during preview when the teachers
2572 // wants to see just what the students see
2573 $options->responses = true;
2574 $options->scores = true;
2575 $options->feedback = true;
2576 $options->correct_responses = true;
2577 $options->solutions = false;
2578 return $options;
2580 if ((time() - $attempt->timefinish) < 120) {
2581 $options->responses = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_RESPONSES) ? 1 : 0;
2582 $options->scores = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_SCORES) ? 1 : 0;
2583 $options->feedback = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_FEEDBACK) ? 1 : 0;
2584 $options->correct_responses = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_ANSWERS) ? 1 : 0;
2585 $options->solutions = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_SOLUTIONS) ? 1 : 0;
2586 } else if (time() < $quiz->timeclose) {
2587 $options->responses = ($quiz->review & QUIZ_REVIEW_OPEN & QUIZ_REVIEW_RESPONSES) ? 1 : 0;
2588 $options->scores = ($quiz->review & QUIZ_REVIEW_OPEN & QUIZ_REVIEW_SCORES) ? 1 : 0;
2589 $options->feedback = ($quiz->review & QUIZ_REVIEW_OPEN & QUIZ_REVIEW_FEEDBACK) ? 1 : 0;
2590 $options->correct_responses = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_ANSWERS) ? 1 : 0;
2591 $options->solutions = ($quiz->review & QUIZ_REVIEW_OPEN & QUIZ_REVIEW_SOLUTIONS) ? 1 : 0;
2592 } else {
2593 $options->responses = ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_RESPONSES) ? 1 : 0;
2594 $options->scores = ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_SCORES) ? 1 : 0;
2595 $options->feedback = ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_FEEDBACK) ? 1 : 0;
2596 $options->correct_responses = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_ANSWERS) ? 1 : 0;
2597 $options->solutions = ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_SOLUTIONS) ? 1 : 0;
2599 return $options;
2603 * Upgrade states for an attempt to Moodle 1.5 model
2605 * Any state that does not yet have its timestamp set to nonzero has not yet been upgraded from Moodle 1.4
2606 * The reason these are still around is that for large sites it would have taken too long to
2607 * upgrade all states at once. This function sets the timestamp field and creates an entry in the
2608 * quiz_newest_states table.
2609 * @param object $attempt The attempt whose states need upgrading
2611 function quiz_upgrade_states($attempt) {
2612 global $CFG;
2613 // The old quiz model only allowed a single response per quiz attempt so that there will be
2614 // only one state record per question for this attempt.
2616 // We set the timestamp of all states to the timemodified field of the attempt.
2617 execute_sql("UPDATE {$CFG->prefix}quiz_states SET timestamp = '$attempt->timemodified' WHERE attempt = '$attempt->id'", false);
2619 // For each state we create an entry in the quiz_newest_states table, with both newest and
2620 // newgraded pointing to this state.
2621 // Actually we only do this for states whose question is actually listed in $attempt->layout.
2622 // We do not do it for states associated to wrapped questions like for example the questions
2623 // used by a RANDOM question
2624 $newest->attemptid = $attempt->id;
2625 $questionlist = quiz_questions_in_quiz($attempt->layout);
2626 if ($states = get_records_select('quiz_states', "attempt = '$attempt->id' AND question IN ($questionlist)")) {
2627 foreach ($states as $state) {
2628 $newest->newgraded = $state->id;
2629 $newest->newest = $state->id;
2630 $newest->questionid = $state->question;
2631 insert_record('quiz_newest_states', $newest, false);
2637 * Get list of available import or export formats
2639 function get_import_export_formats( $type ) {
2641 global $CFG;
2642 $fileformats = get_list_of_plugins("mod/quiz/format");
2644 $fileformatname=array();
2645 require_once( "format.php" );
2646 foreach ($fileformats as $key => $fileformat) {
2647 $format_file = $CFG->dirroot . "/mod/quiz/format/$fileformat/format.php";
2648 if (file_exists( $format_file ) ) {
2649 require_once( $format_file );
2651 else {
2652 continue;
2654 $classname = "quiz_format_$fileformat";
2655 $format_class = new $classname();
2656 if ($type=='import') {
2657 $provided = $format_class->provide_import();
2659 else {
2660 $provided = $format_class->provide_export();
2662 if ($provided) {
2663 $formatname = get_string($fileformat, 'quiz');
2664 if ($formatname == "[[$fileformat]]") {
2665 $formatname = $fileformat; // Just use the raw folder name
2667 $fileformatnames[$fileformat] = $formatname;
2670 natcasesort($fileformatnames);
2672 return $fileformatnames;