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