MDL-20636 Previewing a truefalse question in deferred feedback mode now works.
[moodle.git] / question / type / questionbase.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
19 /**
20  * This file defines the class {@link question_definition} and its subclasses.
21  *
22  * @package moodlecore
23  * @subpackage questiontypes
24  * @copyright 2009 The Open University
25  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
29 /**
30  * The definition of a question of a particular type.
31  *
32  * This class is a close match to the question table in the database.
33  * Definitions of question of a particular type normally subclass one of the
34  * more specific classes {@link question_with_responses},
35  * {@link question_graded_automatically} or {@link question_information_item}.
36  *
37  * @copyright 2009 The Open University
38  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 abstract class question_definition {
41     /** @var integer id of the question in the datase, or null if this question
42      * is not in the database. */
43     public $id;
45     /** @var integer question category id. */
46     public $category;
48     /** @var integer parent question id. */
49     public $parent = 0;
51     /** @var question_type the question type this question is. */
52     public $qtype;
54     /** @var string question name. */
55     public $name;
57     /** @var string question text. */
58     public $questiontext;
60     /** @var integer question test format. */
61     public $questiontextformat;
63     /** @var string question general feedback. */
64     public $generalfeedback;
66     /** @var number what this quetsion is marked out of, by default. */
67     public $defaultmark = 1;
69     /** @var integer How many question numbers this question consumes. */
70     public $length = 1;
72     /** @var number penalty factor of this question. */
73     public $penalty = 0;
75     /** @var string unique identifier of this question. */
76     public $stamp;
78     /** @var string unique identifier of this version of this question. */
79     public $version;
81     /** @var boolean whethre this question has been deleted/hidden in the question bank. */
82     public $hidden = 0;
84     /** @var integer timestamp when this question was created. */
85     public $timecreated;
87     /** @var integer timestamp when this question was modified. */
88     public $timemodified;
90     /** @var integer userid of the use who created this question. */
91     public $createdby;
93     /** @var integer userid of the use who modified this question. */
94     public $modifiedby;
96     /** @var array of question_hints. */
97     public $hints = array();
99     /**
100      * Constructor. Normally to get a question, you call
101      * {@link question_bank::load_question()}, but questions can be created
102      * directly, for example in unit test code.
103      * @return unknown_type
104      */
105     public function __construct() {
106     }
108     /**
109      * @return the name of the question type (for example multichoice) that this
110      * question is.
111      */
112     public function get_type_name() {
113         return $this->qtype->name();
114     }
116     /**
117      * Creat the appropriate behaviour for an attempt at this quetsion,
118      * given the desired (archetypal) behaviour.
119      *
120      * This default implementation will suit most normal graded questions.
121      *
122      * If your question is of a patricular type, then it may need to do something
123      * different. For example, if your question can only be graded manually, then
124      * it should probably return a manualgraded behaviour, irrespective of
125      * what is asked for.
126      *
127      * If your question wants to do somthing especially complicated is some situations,
128      * then you may wish to return a particular behaviour related to the
129      * one asked for. For example, you migth want to return a
130      * qbehaviour_interactive_adapted_for_myqtype.
131      *
132      * @param question_attempt $qa the attempt we are creating an behaviour for.
133      * @param string $preferredbehaviour the requested type of behaviour.
134      * @return question_behaviour the new behaviour object.
135      */
136     public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
137         return question_engine::make_archetypal_behaviour($preferredbehaviour, $qa);
138     }
140     /**
141      * Initialise the first step of an attempt at this quetsion.
142      *
143      * For example, the multiple choice question type uses this method to
144      * randomly shuffle the choices, if that option has been set in the question.
145      * It then stores that order by calling $step->set_qt_var(...).
146      *
147      * @param question_attempt_step $step the step to be initialised.
148      */
149     public function init_first_step(question_attempt_step $step) {
150     }
152     /**
153      * Generate a brief, plain-text, summary of this question. This is used by
154      * various reports. This should show the particular variant of the question
155      * as presented to students. For example, the calculated quetsion type would
156      * fill in the particular numbers that were presented to the student.
157      * This method will return null if such a summary is not possible, or
158      * inappropriate.
159      * @return string|null a plain text summary of this question.
160      */
161     public function get_question_summary(question_attempt $qa) {
162         return html_to_text($this->format_questiontext($qa), 0, false);
163     }
165     /**
166      * Some questions can return a negative mark if the student gets it wrong.
167      *
168      * This method returns the lowest mark the question can return, on the
169      * fraction scale. that is, where the maximum possible mark is 1.0.
170      *
171      * @return number minimum fraction this question will ever return.
172      */
173     public function get_min_fraction() {
174         return 0;
175     }
177     /**
178      * Given a response, rest the parts that are wrong.
179      * @param array $response a response
180      * @return array a cleaned up response with the wrong bits reset.
181      */
182     public function clear_wrong_from_response(array $response) {
183         return array();
184     }
186     /**
187      * Return the number of subparts of this response that are right.
188      * @param array $response a response
189      * @return array with two elements, the number of correct subparts, and
190      * the total number of subparts.
191      */
192     public function get_num_parts_right(array $response) {
193         return array(null, null);
194     }
196     /**
197      * @return qtype_renderer the renderer to use for outputting this question.
198      */
199     public function get_renderer() {
200         global $PAGE;
201         return $PAGE->get_renderer('qtype_' . $this->qtype->name());
202     }
204     /**
205      * What data may be included in the form submission when a student submits
206      * this question in its current state?
207      *
208      * This information is used in calls to optional_param. The parameter name
209      * has {@link question_attempt::get_field_prefix()} automatically prepended.
210      *
211      * @return array|string variable name => PARAM_... constant, or, as a special case
212      *      that should only be used in unavoidable, the constant question_attempt::USE_RAW_DATA
213      *      meaning take all the raw submitted data belonging to this question.
214      */
215     public abstract function get_expected_data();
217     /**
218      * What data would need to be submitted to get this question correct.
219      * If there is more than one correct answer, this method should just
220      * return one possibility.
221      *
222      * @return array parameter name => value.
223      */
224     public abstract function get_correct_response();
226     /**
227      * Apply {@link format_text()} to some content with appropriate settings for
228      * this question.
229      *
230      * @param string $text some content that needs to be output.
231      * @param boolean $clean Whether the HTML needs to be cleaned. Generally,
232      *      parts of the question do not need to be cleaned, and student input does.
233      * @return string the text formatted for output by format_text.
234      */
235     public function format_text($text, $qa, $component, $filearea, $clean = false) {
236         $formatoptions = new stdClass;
237         $formatoptions->noclean = !$clean;
238         $formatoptions->para = false;
239 // TODO $itemid needs to be an argument too.
240         $text = $qa->rewrite_pluginfile_urls($text, $component, $filearea);
241         return format_text($text, $this->questiontextformat, $formatoptions);
242     }
244     /** @return the result of applying {@link format_text()} to the question text. */
245     public function format_questiontext($qa) {
246         return $this->format_text($this->questiontext, $qa, 'question', 'questiontext');
247     }
249     /** @return the result of applying {@link format_text()} to the general feedback. */
250     public function format_generalfeedback($qa) {
251         return $this->format_text($this->generalfeedback, $qa, 'question', 'generalfeedback');
252     }
256 /**
257  * This class represents a 'question' that actually does not allow the student
258  * to respond, like the description 'question' type.
259  *
260  * @copyright 2009 The Open University
261  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
262  */
263 class question_information_item extends question_definition {
264     public function __construct() {
265         parent::__construct();
266         $this->defaultmark = 0;
267         $this->penalty = 0;
268         $this->length = 0;
269     }
271     public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
272         question_engine::load_behaviour_class('informationitem');
273         return new qbehaviour_informationitem($qa, $preferredbehaviour);
274     }
276     public function get_expected_data() {
277         return array();
278     }
280     public function get_correct_response() {
281         return array();
282     }
284     public function get_question_summary() {
285         return null;
286     }
290 /**
291  * Interface that a {@link question_definition} must implement to be usable by
292  * the manual graded behaviour.
293  *
294  * @copyright 2009 The Open University
295  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
296  */
297 interface question_manually_gradable {
298     /**
299      * Used by many of the behaviours, to work out whether the student's
300      * response to the question is complete. That is, whether the question attempt
301      * should move to the COMPLETE or INCOMPLETE state.
302      *
303      * @param array $response responses, as returned by {@link question_attempt_step::get_qt_data()}.
304      * @return boolean whether this response is a complete answer to this question.
305      */
306     public function is_complete_response(array $response);
308     /**
309      * Use by many of the behaviours to determine whether the student's
310      * response has changed. This is normally used to determine that a new set
311      * of responses can safely be discarded.
312      *
313      * @param array $prevresponse the responses previously recorded for this question,
314      *      as returned by {@link question_attempt_step::get_qt_data()}
315      * @param array $newresponse the new responses, in the same format.
316      * @return boolean whether the two sets of responses are the same - that is
317      *      whether the new set of responses can safely be discarded.
318      */
319     public function is_same_response(array $prevresponse, array $newresponse);
321     /**
322      * Produce a plain text summary of a response.
323      * @param $response a response, as might be passed to {@link grade_response()}.
324      * @return string a plain text summary of that response, that could be used in reports.
325      */
326     public function summarise_response(array $response);
328     /**
329      * Categorise the student's response according to the categories defined by
330      * get_possible_responses.
331      * @param $response a response, as might be passed to {@link grade_response()}.
332      * @return array subpartid => {@link question_classified_response} objects.
333      *      returns an empty array if no analysis is possible.
334      */
335     public function classify_response(array $response);
339 /**
340  * This class is used in the return value from
341  * {@link question_manually_gradable::classify_response()}.
342  *
343  * @copyright 2010 The Open University
344  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
345  */
346 class question_classified_response {
347     /**
348      * @var string the classification of this response the student gave to this
349      * part of the question. Must match one of the responseclasses returned by
350      * {@link question_type::get_possible_responses()}.
351      */
352     public $responseclassid;
353     /** @var string the actual response the student gave to this part. */
354     public $response;
355     /** @var number the fraction this part of the response earned. */
356     public $fraction;
357     /**
358      * Constructor, just an easy way to set the fields.
359      * @param string $responseclassid see the field descriptions above.
360      * @param string $response see the field descriptions above.
361      * @param number $fraction see the field descriptions above.
362      */
363     public function __construct($responseclassid, $response, $fraction) {
364         $this->responseclassid = $responseclassid;
365         $this->response = $response;
366         $this->fraction = $fraction;
367     }
369     public static function no_response() {
370         return new question_classified_response(null, null, null);
371     }
375 /**
376  * Interface that a {@link question_definition} must implement to be usable by
377  * the various automatic grading behaviours.
378  *
379  * @copyright 2009 The Open University
380  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
381  */
382 interface question_automatically_gradable extends question_manually_gradable {
383     /**
384      * Use by many of the behaviours to determine whether the student
385      * has provided enough of an answer for the question to be graded automatically,
386      * or whether it must be considered aborted.
387      *
388      * @param array $response responses, as returned by {@link question_attempt_step::get_qt_data()}.
389      * @return boolean whether this response can be graded.
390      */
391     public function is_gradable_response(array $response);
393     /**
394      * In situations where is_gradable_response() returns false, this method
395      * should generate a description of what the problem is.
396      * @return string the message.
397      */
398     public function get_validation_error(array $response);
400     /**
401      * Grade a response to the question, returning a fraction between get_min_fraction() and 1.0,
402      * and the corresponding state CORRECT, PARTIALLY_CORRECT or INCORRECT.
403      * @param array $response responses, as returned by {@link question_attempt_step::get_qt_data()}.
404      * @return array (number, integer) the fraction, and the state.
405      */
406     public function grade_response(array $response);
408     /**
409      * Get one of the question hints. The question_attempt is passed in case
410      * the question type wants to do something complex. For example, the
411      * multiple choice with multiple responses question type will turn off most
412      * of the hint options if the student has selected too many opitions.
413      * @param integer $hintnumber Which hint to display. Indexed starting from 0
414      * @param question_attempt $qa The question_attempt.
415      */
416     public function get_hint($hintnumber, question_attempt $qa);
418     /**
419      * Generate a brief, plain-text, summary of the correct answer to this question.
420      * This is used by various reports, and can also be useful when testing.
421      * This method will return null if such a summary is not possible, or
422      * inappropriate.
423      * @return string|null a plain text summary of the right answer to this question.
424      */
425     public function get_right_answer_summary();
429 /**
430  * Interface that a {@link question_definition} must implement to be usable by
431  * the interactivecountback behaviour.
432  *
433  * @copyright 2010 The Open University
434  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
435  */
436 interface question_automatically_gradable_with_countback extends question_automatically_gradable {
437     /**
438      * Work out a final grade for this attempt, taking into account all the
439      * tries the student made.
440      * @param array $responses the response for each try. Each element of this
441      * array is a response array, as would be passed to {@link grade_response()}.
442      * There may be between 1 and $totaltries responses.
443      * @param integer $totaltries The maximum number of tries allowed.
444      * @return numeric the fraction that should be awarded for this
445      * sequence of response.
446      */
447     public function compute_final_grade($responses, $totaltries);
451 /**
452  * This class represents a real question. That is, one that is not a
453  * {@link question_information_item}.
454  *
455  * @copyright 2009 The Open University
456  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
457  */
458 abstract class question_with_responses extends question_definition
459         implements question_manually_gradable {
460     function classify_response(array $response) {
461         return array();
462     }
466 /**
467  * This class represents a question that can be graded automatically.
468  *
469  * @copyright 2009 The Open University
470  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
471  */
472 abstract class question_graded_automatically extends question_with_responses
473         implements question_automatically_gradable {
474     /** @var Some question types have the option to show the number of sub-parts correct. */
475     public $shownumcorrect = false;
477     public function is_gradable_response(array $response) {
478         return $this->is_complete_response($response);
479     }
481     public function get_right_answer_summary() {
482         $correctresponse = $this->get_correct_response();
483         if (empty($correctresponse)) {
484             return null;
485         }
486         return $this->summarise_response($correctresponse);
487     }
489     public function get_hint($hintnumber, question_attempt $qa) {
490         if (!isset($this->hints[$hintnumber])) {
491             return null;
492         }
493         return $this->hints[$hintnumber];
494     }
498 /**
499  * This class represents a question that can be graded automatically with
500  * countback grading in interactive mode.
501  *
502  * @copyright 2010 The Open University
503  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
504  */
505 abstract class question_graded_automatically_with_countback
506         extends question_graded_automatically
507         implements question_automatically_gradable_with_countback {
509     public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
510         if ($preferredbehaviour == 'interactive') {
511             return question_engine::make_behaviour('interactivecountback', $qa, $preferredbehaviour);
512         }
513         return question_engine::make_archetypal_behaviour($preferredbehaviour, $qa);
514     }
518 /**
519  * This class represents a question that can be graded automatically by using
520  * a {@link question_grading_strategy}.
521  *
522  * @copyright 2009 The Open University
523  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
524  */
525 abstract class question_graded_by_strategy extends question_graded_automatically {
526     /** @var question_grading_strategy the strategy to use for grading. */
527     protected $gradingstrategy;
529     /** @param question_grading_strategy  $strategy the strategy to use for grading. */
530     public function __construct(question_grading_strategy $strategy) {
531         parent::__construct();
532         $this->gradingstrategy = $strategy;
533     }
535     public function get_correct_response() {
536         $answer = $this->get_correct_answer();
537         if (!$answer) {
538             return array();
539         }
541         return array('answer' => $answer->answer);
542     }
544     /**
545      * Get an answer that contains the feedback and fraction that should be
546      * awarded for this resonse.
547      * @param array $response a response.
548      * @return question_answer the matching answer.
549      */
550     public function get_matching_answer(array $response) {
551         return $this->gradingstrategy->grade($response);
552     }
554     /**
555      * @return question_answer an answer that contains the a response that would
556      *      get full marks.
557      */
558     public function get_correct_answer() {
559         return $this->gradingstrategy->get_correct_answer();
560     }
562     public function grade_response(array $response) {
563         $answer = $this->get_matching_answer($response);
564         if ($answer) {
565             return array($answer->fraction, question_state::graded_state_for_fraction($answer->fraction));
566         } else {
567             return array(0, question_state::$gradedwrong);
568         }
569     }
571     public function classify_response(array $response) {
572         if (empty($response['answer'])) {
573             return array($this->id => question_classified_response::no_response());
574         }
576         $ans = $this->get_matching_answer($response);
577         if (!$ans) {
578             return array($this->id => question_classified_response::no_response());
579         }
580         return array($this->id => new question_classified_response(
581                 $ans->id, $response['answer'], $ans->fraction));
582     }
586 /**
587  * Class to represent a question answer, loaded from the question_answers table
588  * in the database.
589  *
590  * @copyright 2009 The Open University
591  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
592  */
593 class question_answer {
594     /** @var string the answer. */
595     public $answer;
597     /** @var number the fraction this answer is worth. */
598     public $fraction;
600     /** @var string the feedback for this answer. */
601     public $feedback;
603     /**
604      * Constructor.
605      * @param string $answer the answer.
606      * @param number $fraction the fraction this answer is worth.
607      * @param string $feedback the feedback for this answer.
608      */
609     public function __construct($answer, $fraction, $feedback) {
610         $this->answer = $answer;
611         $this->fraction = $fraction;
612         $this->feedback = $feedback;
613     }
617 /**
618  * Class to represent a hint associated with a question.
619  * Used by iteractive mode, etc. A question has an array of these.
620  *
621  * @copyright 2010 The Open University
622  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
623  */
624 class question_hint {
625     /** @var The feedback hint to be shown. */
626     public $hint;
628     /**
629      * Constructor.
630      * @param string $hint The hint text
631      */
632     public function __construct($hint) {
633         $this->hint = $hint;
634     }
636     /**
637      * Create a basic hint from a row loaded from the question_hints table in the database.
638      * @param object $row with $row->hint set.
639      * @return question_hint
640      */
641     public static function load_from_record($row) {
642         return new question_hint($row->hint);
643     }
645     /**
646      * Adjust this display options according to the hint settings.
647      * @param question_display_options $options
648      */
649     public function adjust_display_options(question_display_options $options) {
650         // Do nothing.
651     }
655 /**
656  * An extension of {@link question_hint} for questions like match and multiple
657  * choice with multile answers, where there are options for whether to show the
658  * number of parts right at each stage, and to reset the wrong parts.
659  *
660  * @copyright 2010 The Open University
661  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
662  */
663 class question_hint_with_parts extends question_hint {
664     /** @var boolean option to show the number of sub-parts of the question that were right. */
665     public $shownumcorrect;
667     /** @var boolean option to clear the parts of the question that were wrong on retry. */
668     public $clearwrong;
670     /**
671      * Constructor.
672      * @param string $hint The hint text
673      * @param boolean $shownumcorrect whether the number of right parts should be shown
674      * @param boolean $clearwrong whether the wrong parts should be reset.
675      */
676     public function __construct($hint, $shownumcorrect, $clearwrong) {
677         parent::__construct($hint);
678         $this->shownumcorrect = $shownumcorrect;
679         $this->clearwrong = $clearwrong;
680     }
682     /**
683      * Create a basic hint from a row loaded from the question_hints table in the database.
684      * @param object $row with $row->hint, ->shownumcorrect and ->clearwrong set.
685      * @return question_hint_with_parts
686      */
687     public static function load_from_record($row) {
688         return new question_hint_with_parts($row->hint, $row->shownumcorrect, $row->clearwrong);
689     }
691     public function adjust_display_options(question_display_options $options) {
692         parent::adjust_display_options($options);
693         if ($this->clearwrong) {
694             $options->clearwrong = true;
695         }
696         $options->numpartscorrect = $this->shownumcorrect;
697     }
701 /**
702  * This question_grading_strategy interface. Used to share grading code between
703  * questions that that subclass {@link question_graded_by_strategy}.
704  *
705  * @copyright 2009 The Open University
706  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
707  */
708 interface question_grading_strategy {
709     /**
710      * Return a question answer that describes the outcome (fraction and feeback)
711      * for a particular respons.
712      * @param array $response the response.
713      * @return question_answer the answer describing the outcome.
714      */
715     public function grade(array $response);
717     /**
718      * @return question_answer an answer that contains the a response that would
719      *      get full marks.
720      */
721     public function get_correct_answer();
725 /**
726  * This interface defines the methods that a {@link question_definition} must
727  * implement if it is to be graded by the
728  * {@link question_first_matching_answer_grading_strategy}.
729  *
730  * @copyright 2009 The Open University
731  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
732  */
733 interface question_response_answer_comparer {
734     /** @return array of {@link question_answers}. */
735     public function get_answers();
737     /**
738      * @param array $response the response.
739      * @param question_answer $answer an answer.
740      * @return boolean whether the response matches the answer.
741      */
742     public function compare_response_with_answer(array $response, question_answer $answer);
746 /**
747  * This grading strategy is used by question types like shortanswer an numerical.
748  * It gets a list of possible answers from the question, and returns the first one
749  * that matches the given response. It returns the first answer with fraction 1.0
750  * when asked for the correct answer.
751  *
752  * @copyright 2009 The Open University
753  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
754  */
755 class question_first_matching_answer_grading_strategy implements question_grading_strategy {
756     /**
757      * @var question_response_answer_comparer (presumably also a
758      * {@link question_definition}) the question we are doing the grading for.
759      */
760     protected $question;
762     /**
763      * @param question_response_answer_comparer $question (presumably also a
764      * {@link question_definition}) the question we are doing the grading for.
765      */
766     public function __construct(question_response_answer_comparer $question) {
767         $this->question = $question;
768     }
770     public function grade(array $response) {
771         foreach ($this->question->get_answers() as $aid => $answer) {
772             if ($this->question->compare_response_with_answer($response, $answer)) {
773                 $answer->id = $aid;
774                 return $answer;
775             }
776         }
777         return null;
778     }
780     public function get_correct_answer() {
781         foreach ($this->question->get_answers() as $answer) {
782             $state = question_state::graded_state_for_fraction($answer->fraction);
783             if ($state == question_state::$gradedright) {
784                 return $answer;
785             }
786         }
787         return null;
788     }