4ae64584178b2d6cc1f3a730381d6cdd390bb9cc
[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/>.
18 /**
19  * This file defines the class {@link question_definition} and its subclasses.
20  *
21  * @package    moodlecore
22  * @subpackage questiontypes
23  * @copyright  2009 The Open University
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
28 defined('MOODLE_INTERNAL') || die();
31 /**
32  * The definition of a question of a particular type.
33  *
34  * This class is a close match to the question table in the database.
35  * Definitions of question of a particular type normally subclass one of the
36  * more specific classes {@link question_with_responses},
37  * {@link question_graded_automatically} or {@link question_information_item}.
38  *
39  * @copyright  2009 The Open University
40  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41  */
42 abstract class question_definition {
43     /** @var integer id of the question in the datase, or null if this question
44      * is not in the database. */
45     public $id;
47     /** @var integer question category id. */
48     public $category;
50     /** @var integer question category id. */
51     public $contextid;
53     /** @var integer parent question id. */
54     public $parent = 0;
56     /** @var question_type the question type this question is. */
57     public $qtype;
59     /** @var string question name. */
60     public $name;
62     /** @var string question text. */
63     public $questiontext;
65     /** @var integer question test format. */
66     public $questiontextformat;
68     /** @var string question general feedback. */
69     public $generalfeedback;
71     /** @var integer question test format. */
72     public $generalfeedbackformat;
74     /** @var number what this quetsion is marked out of, by default. */
75     public $defaultmark = 1;
77     /** @var integer How many question numbers this question consumes. */
78     public $length = 1;
80     /** @var number penalty factor of this question. */
81     public $penalty = 0;
83     /** @var string unique identifier of this question. */
84     public $stamp;
86     /** @var string unique identifier of this version of this question. */
87     public $version;
89     /** @var boolean whethre this question has been deleted/hidden in the question bank. */
90     public $hidden = 0;
92     /** @var integer timestamp when this question was created. */
93     public $timecreated;
95     /** @var integer timestamp when this question was modified. */
96     public $timemodified;
98     /** @var integer userid of the use who created this question. */
99     public $createdby;
101     /** @var integer userid of the use who modified this question. */
102     public $modifiedby;
104     /** @var array of question_hints. */
105     public $hints = array();
107     /**
108      * Constructor. Normally to get a question, you call
109      * {@link question_bank::load_question()}, but questions can be created
110      * directly, for example in unit test code.
111      * @return unknown_type
112      */
113     public function __construct() {
114     }
116     /**
117      * @return the name of the question type (for example multichoice) that this
118      * question is.
119      */
120     public function get_type_name() {
121         return $this->qtype->name();
122     }
124     /**
125      * Creat the appropriate behaviour for an attempt at this quetsion,
126      * given the desired (archetypal) behaviour.
127      *
128      * This default implementation will suit most normal graded questions.
129      *
130      * If your question is of a patricular type, then it may need to do something
131      * different. For example, if your question can only be graded manually, then
132      * it should probably return a manualgraded behaviour, irrespective of
133      * what is asked for.
134      *
135      * If your question wants to do somthing especially complicated is some situations,
136      * then you may wish to return a particular behaviour related to the
137      * one asked for. For example, you migth want to return a
138      * qbehaviour_interactive_adapted_for_myqtype.
139      *
140      * @param question_attempt $qa the attempt we are creating an behaviour for.
141      * @param string $preferredbehaviour the requested type of behaviour.
142      * @return question_behaviour the new behaviour object.
143      */
144     public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
145         return question_engine::make_archetypal_behaviour($preferredbehaviour, $qa);
146     }
148     /**
149      * Start a new attempt at this question, storing any information that will
150      * be needed later in the step.
151      *
152      * This is where the question can do any initialisation required on a
153      * per-attempt basis. For example, this is where the multiple choice
154      * question type randomly shuffles the choices (if that option is set).
155      *
156      * Any information about how the question has been set up for this attempt
157      * should be stored in the $step, by calling $step->set_qt_var(...).
158      *
159      * @param question_attempt_step The first step of the {@link question_attempt}
160      *      being started. Can be used to store state.
161      */
162     public function start_attempt(question_attempt_step $step) {
163     }
165     /**
166      * When an in-progress {@link question_attempt} is re-loaded from the
167      * database, this method is called so that the question can re-initialise
168      * its internal state as needed by this attempt.
169      *
170      * For example, the multiple choice question type needs to set the order
171      * of the choices to the order that was set up when start_attempt was called
172      * originally. All the information required to do this should be in the
173      * $step object, which is the first step of the question_attempt being loaded.
174      *
175      * @param question_attempt_step The first step of the {@link question_attempt}
176      *      being loaded.
177      */
178     public function apply_attempt_state(question_attempt_step $step) {
179     }
181     /**
182      * Generate a brief, plain-text, summary of this question. This is used by
183      * various reports. This should show the particular variant of the question
184      * as presented to students. For example, the calculated quetsion type would
185      * fill in the particular numbers that were presented to the student.
186      * This method will return null if such a summary is not possible, or
187      * inappropriate.
188      * @return string|null a plain text summary of this question.
189      */
190     public function get_question_summary() {
191         return $this->html_to_text($this->questiontext, $this->questiontextformat);
192     }
194     /**
195      * Some questions can return a negative mark if the student gets it wrong.
196      *
197      * This method returns the lowest mark the question can return, on the
198      * fraction scale. that is, where the maximum possible mark is 1.0.
199      *
200      * @return number minimum fraction this question will ever return.
201      */
202     public function get_min_fraction() {
203         return 0;
204     }
206     /**
207      * Given a response, rest the parts that are wrong.
208      * @param array $response a response
209      * @return array a cleaned up response with the wrong bits reset.
210      */
211     public function clear_wrong_from_response(array $response) {
212         return array();
213     }
215     /**
216      * Return the number of subparts of this response that are right.
217      * @param array $response a response
218      * @return array with two elements, the number of correct subparts, and
219      * the total number of subparts.
220      */
221     public function get_num_parts_right(array $response) {
222         return array(null, null);
223     }
225     /**
226      * @param moodle_page the page we are outputting to.
227      * @return qtype_renderer the renderer to use for outputting this question.
228      */
229     public function get_renderer(moodle_page $page) {
230         return $page->get_renderer($this->qtype->plugin_name());
231     }
233     /**
234      * What data may be included in the form submission when a student submits
235      * this question in its current state?
236      *
237      * This information is used in calls to optional_param. The parameter name
238      * has {@link question_attempt::get_field_prefix()} automatically prepended.
239      *
240      * @return array|string variable name => PARAM_... constant, or, as a special case
241      *      that should only be used in unavoidable, the constant question_attempt::USE_RAW_DATA
242      *      meaning take all the raw submitted data belonging to this question.
243      */
244     public abstract function get_expected_data();
246     /**
247      * What data would need to be submitted to get this question correct.
248      * If there is more than one correct answer, this method should just
249      * return one possibility.
250      *
251      * @return array parameter name => value.
252      */
253     public abstract function get_correct_response();
255     /**
256      * Apply {@link format_text()} to some content with appropriate settings for
257      * this question.
258      *
259      * @param string $text some content that needs to be output.
260      * @param int $format the FORMAT_... constant.
261      * @param question_attempt $qa the question attempt.
262      * @param string $component used for rewriting file area URLs.
263      * @param string $filearea used for rewriting file area URLs.
264      * @param bool $clean Whether the HTML needs to be cleaned. Generally,
265      *      parts of the question do not need to be cleaned, and student input does.
266      * @return string the text formatted for output by format_text.
267      */
268     public function format_text($text, $format, $qa, $component, $filearea, $itemid, $clean = false) {
269         $formatoptions = new stdClass();
270         $formatoptions->noclean = !$clean;
271         $formatoptions->para = false;
272         $text = $qa->rewrite_pluginfile_urls($text, $component, $filearea, $itemid);
273         return format_text($text, $format, $formatoptions);
274     }
276     /**
277      * Convert some part of the question text to plain text. This might be used,
278      * for example, by get_response_summary().
279      * @param string $text The HTML to reduce to plain text.
280      * @param int $format the FORMAT_... constant.
281      * @return string the equivalent plain text.
282      */
283     public function html_to_text($text, $format) {
284         $formatoptions = new stdClass();
285         $formatoptions->noclean = true;
286         return html_to_text(format_text($text, $format, $formatoptions), 0, false);
287     }
289     /** @return the result of applying {@link format_text()} to the question text. */
290     public function format_questiontext($qa) {
291         return $this->format_text($this->questiontext, $this->questiontextformat,
292                 $qa, 'question', 'questiontext', $this->id);
293     }
295     /** @return the result of applying {@link format_text()} to the general feedback. */
296     public function format_generalfeedback($qa) {
297         return $this->format_text($this->generalfeedback, $this->generalfeedbackformat,
298                 $qa, 'question', 'generalfeedback', $this->id);
299     }
301     /**
302      * Checks whether the users is allow to be served a particular file.
303      * @param question_attempt $qa the question attempt being displayed.
304      * @param question_display_options $options the options that control display of the question.
305      * @param string $component the name of the component we are serving files for.
306      * @param string $filearea the name of the file area.
307      * @param array $args the remaining bits of the file path.
308      * @param bool $forcedownload whether the user must be forced to download the file.
309      * @return bool true if the user can access this file.
310      */
311     public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
312         if ($component == 'question' && $filearea == 'questiontext') {
313             // Question text always visible.
314             return true;
316         } else if ($component == 'question' && $filearea == 'generalfeedback') {
317             return $options->generalfeedback;
319         } else {
320             // Unrecognised component or filearea.
321             return false;
322         }
323     }
327 /**
328  * This class represents a 'question' that actually does not allow the student
329  * to respond, like the description 'question' type.
330  *
331  * @copyright  2009 The Open University
332  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
333  */
334 class question_information_item extends question_definition {
335     public function __construct() {
336         parent::__construct();
337         $this->defaultmark = 0;
338         $this->penalty = 0;
339         $this->length = 0;
340     }
342     public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
343         question_engine::load_behaviour_class('informationitem');
344         return new qbehaviour_informationitem($qa, $preferredbehaviour);
345     }
347     public function get_expected_data() {
348         return array();
349     }
351     public function get_correct_response() {
352         return array();
353     }
355     public function get_question_summary() {
356         return null;
357     }
361 /**
362  * Interface that a {@link question_definition} must implement to be usable by
363  * the manual graded behaviour.
364  *
365  * @copyright  2009 The Open University
366  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
367  */
368 interface question_manually_gradable {
369     /**
370      * Used by many of the behaviours, to work out whether the student's
371      * response to the question is complete. That is, whether the question attempt
372      * should move to the COMPLETE or INCOMPLETE state.
373      *
374      * @param array $response responses, as returned by {@link question_attempt_step::get_qt_data()}.
375      * @return bool whether this response is a complete answer to this question.
376      */
377     public function is_complete_response(array $response);
379     /**
380      * Use by many of the behaviours to determine whether the student's
381      * response has changed. This is normally used to determine that a new set
382      * of responses can safely be discarded.
383      *
384      * @param array $prevresponse the responses previously recorded for this question,
385      *      as returned by {@link question_attempt_step::get_qt_data()}
386      * @param array $newresponse the new responses, in the same format.
387      * @return bool whether the two sets of responses are the same - that is
388      *      whether the new set of responses can safely be discarded.
389      */
390     public function is_same_response(array $prevresponse, array $newresponse);
392     /**
393      * Produce a plain text summary of a response.
394      * @param $response a response, as might be passed to {@link grade_response()}.
395      * @return string a plain text summary of that response, that could be used in reports.
396      */
397     public function summarise_response(array $response);
399     /**
400      * Categorise the student's response according to the categories defined by
401      * get_possible_responses.
402      * @param $response a response, as might be passed to {@link grade_response()}.
403      * @return array subpartid => {@link question_classified_response} objects.
404      *      returns an empty array if no analysis is possible.
405      */
406     public function classify_response(array $response);
410 /**
411  * This class is used in the return value from
412  * {@link question_manually_gradable::classify_response()}.
413  *
414  * @copyright  2010 The Open University
415  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
416  */
417 class question_classified_response {
418     /**
419      * @var string the classification of this response the student gave to this
420      * part of the question. Must match one of the responseclasses returned by
421      * {@link question_type::get_possible_responses()}.
422      */
423     public $responseclassid;
424     /** @var string the actual response the student gave to this part. */
425     public $response;
426     /** @var number the fraction this part of the response earned. */
427     public $fraction;
428     /**
429      * Constructor, just an easy way to set the fields.
430      * @param string $responseclassid see the field descriptions above.
431      * @param string $response see the field descriptions above.
432      * @param number $fraction see the field descriptions above.
433      */
434     public function __construct($responseclassid, $response, $fraction) {
435         $this->responseclassid = $responseclassid;
436         $this->response = $response;
437         $this->fraction = $fraction;
438     }
440     public static function no_response() {
441         return new question_classified_response(null, null, null);
442     }
446 /**
447  * Interface that a {@link question_definition} must implement to be usable by
448  * the various automatic grading behaviours.
449  *
450  * @copyright  2009 The Open University
451  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
452  */
453 interface question_automatically_gradable extends question_manually_gradable {
454     /**
455      * Use by many of the behaviours to determine whether the student
456      * has provided enough of an answer for the question to be graded automatically,
457      * or whether it must be considered aborted.
458      *
459      * @param array $response responses, as returned by {@link question_attempt_step::get_qt_data()}.
460      * @return bool whether this response can be graded.
461      */
462     public function is_gradable_response(array $response);
464     /**
465      * In situations where is_gradable_response() returns false, this method
466      * should generate a description of what the problem is.
467      * @return string the message.
468      */
469     public function get_validation_error(array $response);
471     /**
472      * Grade a response to the question, returning a fraction between get_min_fraction() and 1.0,
473      * and the corresponding state CORRECT, PARTIALLY_CORRECT or INCORRECT.
474      * @param array $response responses, as returned by {@link question_attempt_step::get_qt_data()}.
475      * @return array (number, integer) the fraction, and the state.
476      */
477     public function grade_response(array $response);
479     /**
480      * Get one of the question hints. The question_attempt is passed in case
481      * the question type wants to do something complex. For example, the
482      * multiple choice with multiple responses question type will turn off most
483      * of the hint options if the student has selected too many opitions.
484      * @param int $hintnumber Which hint to display. Indexed starting from 0
485      * @param question_attempt $qa The question_attempt.
486      */
487     public function get_hint($hintnumber, question_attempt $qa);
489     /**
490      * Generate a brief, plain-text, summary of the correct answer to this question.
491      * This is used by various reports, and can also be useful when testing.
492      * This method will return null if such a summary is not possible, or
493      * inappropriate.
494      * @return string|null a plain text summary of the right answer to this question.
495      */
496     public function get_right_answer_summary();
500 /**
501  * Interface that a {@link question_definition} must implement to be usable by
502  * the interactivecountback behaviour.
503  *
504  * @copyright  2010 The Open University
505  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
506  */
507 interface question_automatically_gradable_with_countback extends question_automatically_gradable {
508     /**
509      * Work out a final grade for this attempt, taking into account all the
510      * tries the student made.
511      * @param array $responses the response for each try. Each element of this
512      * array is a response array, as would be passed to {@link grade_response()}.
513      * There may be between 1 and $totaltries responses.
514      * @param int $totaltries The maximum number of tries allowed.
515      * @return numeric the fraction that should be awarded for this
516      * sequence of response.
517      */
518     public function compute_final_grade($responses, $totaltries);
522 /**
523  * This class represents a real question. That is, one that is not a
524  * {@link question_information_item}.
525  *
526  * @copyright  2009 The Open University
527  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
528  */
529 abstract class question_with_responses extends question_definition
530         implements question_manually_gradable {
531     public function classify_response(array $response) {
532         return array();
533     }
537 /**
538  * This class represents a question that can be graded automatically.
539  *
540  * @copyright  2009 The Open University
541  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
542  */
543 abstract class question_graded_automatically extends question_with_responses
544         implements question_automatically_gradable {
545     /** @var Some question types have the option to show the number of sub-parts correct. */
546     public $shownumcorrect = false;
548     public function is_gradable_response(array $response) {
549         return $this->is_complete_response($response);
550     }
552     public function get_right_answer_summary() {
553         $correctresponse = $this->get_correct_response();
554         if (empty($correctresponse)) {
555             return null;
556         }
557         return $this->summarise_response($correctresponse);
558     }
560     /**
561      * Check a request for access to a file belonging to a combined feedback field.
562      * @param question_attempt $qa the question attempt being displayed.
563      * @param question_display_options $options the options that control display of the question.
564      * @param string $filearea the name of the file area.
565      * @return bool whether access to the file should be allowed.
566      */
567     protected function check_combined_feedback_file_access($qa, $options, $filearea) {
568         $state = $qa->get_state();
570         if (!$state->is_finished()) {
571             $response = $qa->get_last_qt_data();
572             if (!$this->is_gradable_response($response)) {
573                 return false;
574             }
575             list($notused, $state) = $this->grade_response($response);
576         }
578         return $options->feedback && $state->get_feedback_class() . 'feedback' == $filearea;
579     }
581     /**
582      * Check a request for access to a file belonging to a hint.
583      * @param question_attempt $qa the question attempt being displayed.
584      * @param question_display_options $options the options that control display of the question.
585      * @param array $args the remaining bits of the file path.
586      * @return bool whether access to the file should be allowed.
587      */
588     protected function check_hint_file_access($qa, $options, $args) {
589         if (!$options->feedback) {
590             return false;
591         }
592         $hint = $qa->get_applicable_hint();
593         $hintid = reset($args); // itemid is hint id.
594         return $hintid == $hint->id;
595     }
597     public function get_hint($hintnumber, question_attempt $qa) {
598         if (!isset($this->hints[$hintnumber])) {
599             return null;
600         }
601         return $this->hints[$hintnumber];
602     }
604     public function format_hint(question_hint $hint, question_attempt $qa) {
605         return $this->format_text($hint->hint, $hint->hintformat, $qa, 'question', 'hint', $hint->id);
606     }
610 /**
611  * This class represents a question that can be graded automatically with
612  * countback grading in interactive mode.
613  *
614  * @copyright  2010 The Open University
615  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
616  */
617 abstract class question_graded_automatically_with_countback
618         extends question_graded_automatically
619         implements question_automatically_gradable_with_countback {
621     public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
622         if ($preferredbehaviour == 'interactive') {
623             return question_engine::make_behaviour('interactivecountback', $qa, $preferredbehaviour);
624         }
625         return question_engine::make_archetypal_behaviour($preferredbehaviour, $qa);
626     }
630 /**
631  * This class represents a question that can be graded automatically by using
632  * a {@link question_grading_strategy}.
633  *
634  * @copyright  2009 The Open University
635  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
636  */
637 abstract class question_graded_by_strategy extends question_graded_automatically {
638     /** @var question_grading_strategy the strategy to use for grading. */
639     protected $gradingstrategy;
641     /** @param question_grading_strategy  $strategy the strategy to use for grading. */
642     public function __construct(question_grading_strategy $strategy) {
643         parent::__construct();
644         $this->gradingstrategy = $strategy;
645     }
647     public function get_correct_response() {
648         $answer = $this->get_correct_answer();
649         if (!$answer) {
650             return array();
651         }
653         return array('answer' => $answer->answer);
654     }
656     /**
657      * Get an answer that contains the feedback and fraction that should be
658      * awarded for this resonse.
659      * @param array $response a response.
660      * @return question_answer the matching answer.
661      */
662     public function get_matching_answer(array $response) {
663         return $this->gradingstrategy->grade($response);
664     }
666     /**
667      * @return question_answer an answer that contains the a response that would
668      *      get full marks.
669      */
670     public function get_correct_answer() {
671         return $this->gradingstrategy->get_correct_answer();
672     }
674     public function grade_response(array $response) {
675         $answer = $this->get_matching_answer($response);
676         if ($answer) {
677             return array($answer->fraction, question_state::graded_state_for_fraction($answer->fraction));
678         } else {
679             return array(0, question_state::$gradedwrong);
680         }
681     }
683     public function classify_response(array $response) {
684         if (empty($response['answer'])) {
685             return array($this->id => question_classified_response::no_response());
686         }
688         $ans = $this->get_matching_answer($response);
689         if (!$ans) {
690             return array($this->id => question_classified_response::no_response());
691         }
692         return array($this->id => new question_classified_response(
693                 $ans->id, $response['answer'], $ans->fraction));
694     }
698 /**
699  * Class to represent a question answer, loaded from the question_answers table
700  * in the database.
701  *
702  * @copyright  2009 The Open University
703  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
704  */
705 class question_answer {
706     /** @var integer the answer id. */
707     public $id;
709     /** @var string the answer. */
710     public $answer;
712     /** @var integer one of the FORMAT_... constans. */
713     public $answerformat = FORMAT_PLAIN;
715     /** @var number the fraction this answer is worth. */
716     public $fraction;
718     /** @var string the feedback for this answer. */
719     public $feedback;
721     /** @var integer one of the FORMAT_... constans. */
722     public $feedbackformat;
724     /**
725      * Constructor.
726      * @param int $id the answer.
727      * @param string $answer the answer.
728      * @param int $answerformat the format of the answer.
729      * @param number $fraction the fraction this answer is worth.
730      * @param string $feedback the feedback for this answer.
731      * @param int $feedbackformat the format of the feedback.
732      */
733     public function __construct($id, $answer, $fraction, $feedback, $feedbackformat) {
734         $this->id = $id;
735         $this->answer = $answer;
736         $this->fraction = $fraction;
737         $this->feedback = $feedback;
738         $this->feedbackformat = $feedbackformat;
739     }
743 /**
744  * Class to represent a hint associated with a question.
745  * Used by iteractive mode, etc. A question has an array of these.
746  *
747  * @copyright  2010 The Open University
748  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
749  */
750 class question_hint {
751     /** @var integer The hint id. */
752     public $id;
753     /** @var string The feedback hint to be shown. */
754     public $hint;
755     /** @var integer The corresponding text FORMAT_... type. */
756     public $hintformat;
758     /**
759      * Constructor.
760      * @param int the hint id from the database.
761      * @param string $hint The hint text
762      * @param int the corresponding text FORMAT_... type.
763      */
764     public function __construct($id, $hint, $hintformat) {
765         $this->id = $id;
766         $this->hint = $hint;
767         $this->hintformat = $hintformat;
768     }
770     /**
771      * Create a basic hint from a row loaded from the question_hints table in the database.
772      * @param object $row with $row->hint set.
773      * @return question_hint
774      */
775     public static function load_from_record($row) {
776         return new question_hint($row->id, $row->hint, $row->hintformat);
777     }
779     /**
780      * Adjust this display options according to the hint settings.
781      * @param question_display_options $options
782      */
783     public function adjust_display_options(question_display_options $options) {
784         // Do nothing.
785     }
789 /**
790  * An extension of {@link question_hint} for questions like match and multiple
791  * choice with multile answers, where there are options for whether to show the
792  * number of parts right at each stage, and to reset the wrong parts.
793  *
794  * @copyright  2010 The Open University
795  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
796  */
797 class question_hint_with_parts extends question_hint {
798     /** @var boolean option to show the number of sub-parts of the question that were right. */
799     public $shownumcorrect;
801     /** @var boolean option to clear the parts of the question that were wrong on retry. */
802     public $clearwrong;
804     /**
805      * Constructor.
806      * @param int the hint id from the database.
807      * @param string $hint The hint text
808      * @param int the corresponding text FORMAT_... type.
809      * @param bool $shownumcorrect whether the number of right parts should be shown
810      * @param bool $clearwrong whether the wrong parts should be reset.
811      */
812     public function __construct($id, $hint, $hintformat, $shownumcorrect, $clearwrong) {
813         parent::__construct($id, $hint, $hintformat);
814         $this->shownumcorrect = $shownumcorrect;
815         $this->clearwrong = $clearwrong;
816     }
818     /**
819      * Create a basic hint from a row loaded from the question_hints table in the database.
820      * @param object $row with $row->hint, ->shownumcorrect and ->clearwrong set.
821      * @return question_hint_with_parts
822      */
823     public static function load_from_record($row) {
824         return new question_hint_with_parts($row->id, $row->hint, $row->hintformat,
825                 $row->shownumcorrect, $row->clearwrong);
826     }
828     public function adjust_display_options(question_display_options $options) {
829         parent::adjust_display_options($options);
830         if ($this->clearwrong) {
831             $options->clearwrong = true;
832         }
833         $options->numpartscorrect = $this->shownumcorrect;
834     }
838 /**
839  * This question_grading_strategy interface. Used to share grading code between
840  * questions that that subclass {@link question_graded_by_strategy}.
841  *
842  * @copyright  2009 The Open University
843  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
844  */
845 interface question_grading_strategy {
846     /**
847      * Return a question answer that describes the outcome (fraction and feeback)
848      * for a particular respons.
849      * @param array $response the response.
850      * @return question_answer the answer describing the outcome.
851      */
852     public function grade(array $response);
854     /**
855      * @return question_answer an answer that contains the a response that would
856      *      get full marks.
857      */
858     public function get_correct_answer();
862 /**
863  * This interface defines the methods that a {@link question_definition} must
864  * implement if it is to be graded by the
865  * {@link question_first_matching_answer_grading_strategy}.
866  *
867  * @copyright  2009 The Open University
868  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
869  */
870 interface question_response_answer_comparer {
871     /** @return array of {@link question_answers}. */
872     public function get_answers();
874     /**
875      * @param array $response the response.
876      * @param question_answer $answer an answer.
877      * @return bool whether the response matches the answer.
878      */
879     public function compare_response_with_answer(array $response, question_answer $answer);
883 /**
884  * This grading strategy is used by question types like shortanswer an numerical.
885  * It gets a list of possible answers from the question, and returns the first one
886  * that matches the given response. It returns the first answer with fraction 1.0
887  * when asked for the correct answer.
888  *
889  * @copyright  2009 The Open University
890  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
891  */
892 class question_first_matching_answer_grading_strategy implements question_grading_strategy {
893     /**
894      * @var question_response_answer_comparer (presumably also a
895      * {@link question_definition}) the question we are doing the grading for.
896      */
897     protected $question;
899     /**
900      * @param question_response_answer_comparer $question (presumably also a
901      * {@link question_definition}) the question we are doing the grading for.
902      */
903     public function __construct(question_response_answer_comparer $question) {
904         $this->question = $question;
905     }
907     public function grade(array $response) {
908         foreach ($this->question->get_answers() as $aid => $answer) {
909             if ($this->question->compare_response_with_answer($response, $answer)) {
910                 $answer->id = $aid;
911                 return $answer;
912             }
913         }
914         return null;
915     }
917     public function get_correct_answer() {
918         foreach ($this->question->get_answers() as $answer) {
919             $state = question_state::graded_state_for_fraction($answer->fraction);
920             if ($state == question_state::$gradedright) {
921                 return $answer;
922             }
923         }
924         return null;
925     }