MDL-20636 Start supporting different response formats in essay. #216
[moodle.git] / question / type / questionbase.php
CommitLineData
d1b7e03d
TH
1<?php
2
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/>.
17
d1b7e03d
TH
18/**
19 * This file defines the class {@link question_definition} and its subclasses.
20 *
b04a4319 21 * @package moodlecore
d1b7e03d 22 * @subpackage questiontypes
b04a4319
TH
23 * @copyright 2009 The Open University
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
25 */
26
27
a17b297d
TH
28defined('MOODLE_INTERNAL') || die();
29
30
d1b7e03d
TH
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 *
7764183a
TH
39 * @copyright 2009 The Open University
40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
41 */
42abstract 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;
46
47 /** @var integer question category id. */
48 public $category;
49
56e82d99
TH
50 /** @var integer question category id. */
51 public $contextid;
52
d1b7e03d
TH
53 /** @var integer parent question id. */
54 public $parent = 0;
55
56 /** @var question_type the question type this question is. */
57 public $qtype;
58
59 /** @var string question name. */
60 public $name;
61
62 /** @var string question text. */
63 public $questiontext;
64
65 /** @var integer question test format. */
66 public $questiontextformat;
67
68 /** @var string question general feedback. */
69 public $generalfeedback;
70
1c2ed7c5
TH
71 /** @var integer question test format. */
72 public $generalfeedbackformat;
73
d1b7e03d
TH
74 /** @var number what this quetsion is marked out of, by default. */
75 public $defaultmark = 1;
76
77 /** @var integer How many question numbers this question consumes. */
78 public $length = 1;
79
80 /** @var number penalty factor of this question. */
81 public $penalty = 0;
82
83 /** @var string unique identifier of this question. */
84 public $stamp;
85
86 /** @var string unique identifier of this version of this question. */
87 public $version;
88
89 /** @var boolean whethre this question has been deleted/hidden in the question bank. */
90 public $hidden = 0;
91
92 /** @var integer timestamp when this question was created. */
93 public $timecreated;
94
95 /** @var integer timestamp when this question was modified. */
96 public $timemodified;
97
98 /** @var integer userid of the use who created this question. */
99 public $createdby;
100
101 /** @var integer userid of the use who modified this question. */
102 public $modifiedby;
103
104 /** @var array of question_hints. */
105 public $hints = array();
106
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 }
115
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 }
123
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 }
147
148 /**
ef31a283
TH
149 * Start a new attempt at this question, storing any information that will
150 * be needed later in the step.
d1b7e03d 151 *
ef31a283
TH
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).
d1b7e03d 155 *
ef31a283
TH
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 }
164
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.
d1b7e03d 177 */
ef31a283 178 public function apply_attempt_state(question_attempt_step $step) {
d1b7e03d
TH
179 }
180
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 */
ec3d4ef5 190 public function get_question_summary() {
22cebed5 191 return $this->html_to_text($this->questiontext, $this->questiontextformat);
d1b7e03d
TH
192 }
193
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 }
205
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 }
214
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 }
224
225 /**
b36d2d06 226 * @param moodle_page the page we are outputting to.
d1b7e03d
TH
227 * @return qtype_renderer the renderer to use for outputting this question.
228 */
2daffca5
TH
229 public function get_renderer(moodle_page $page) {
230 return $page->get_renderer($this->qtype->plugin_name());
d1b7e03d
TH
231 }
232
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();
245
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();
254
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.
22cebed5 260 * @param int $format the FORMAT_... constant.
068b4594
TH
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.
f7970e3c 264 * @param bool $clean Whether the HTML needs to be cleaned. Generally,
d1b7e03d
TH
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 */
22cebed5 268 public function format_text($text, $format, $qa, $component, $filearea, $itemid, $clean = false) {
0ff4bd08 269 $formatoptions = new stdClass();
d1b7e03d
TH
270 $formatoptions->noclean = !$clean;
271 $formatoptions->para = false;
7a719748 272 $text = $qa->rewrite_pluginfile_urls($text, $component, $filearea, $itemid);
22cebed5 273 return format_text($text, $format, $formatoptions);
d1b7e03d
TH
274 }
275
ec3d4ef5
TH
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.
22cebed5
TH
280 * @param int $format the FORMAT_... constant.
281 * @return string the equivalent plain text.
ec3d4ef5 282 */
22cebed5 283 public function html_to_text($text, $format) {
0ff4bd08 284 $formatoptions = new stdClass();
ec3d4ef5 285 $formatoptions->noclean = true;
22cebed5 286 return html_to_text(format_text($text, $format, $formatoptions), 0, false);
ec3d4ef5
TH
287 }
288
d1b7e03d 289 /** @return the result of applying {@link format_text()} to the question text. */
2b7da645 290 public function format_questiontext($qa) {
22cebed5
TH
291 return $this->format_text($this->questiontext, $this->questiontextformat,
292 $qa, 'question', 'questiontext', $this->id);
d1b7e03d
TH
293 }
294
295 /** @return the result of applying {@link format_text()} to the general feedback. */
2b7da645 296 public function format_generalfeedback($qa) {
22cebed5
TH
297 return $this->format_text($this->generalfeedback, $this->generalfeedbackformat,
298 $qa, 'question', 'generalfeedback', $this->id);
7a719748
TH
299 }
300
301 /**
302 * Checks whether the users is allow to be served a particular file.
93cadb1e 303 * @param question_attempt $qa the question attempt being displayed.
7a719748
TH
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.
f7970e3c
TH
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.
7a719748
TH
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;
315
316 } else if ($component == 'question' && $filearea == 'generalfeedback') {
317 return $options->generalfeedback;
318
319 } else {
320 // Unrecognised component or filearea.
321 return false;
322 }
d1b7e03d
TH
323 }
324}
325
326
327/**
328 * This class represents a 'question' that actually does not allow the student
329 * to respond, like the description 'question' type.
330 *
7764183a
TH
331 * @copyright 2009 The Open University
332 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
333 */
334class 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 }
341
342 public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
343 question_engine::load_behaviour_class('informationitem');
344 return new qbehaviour_informationitem($qa, $preferredbehaviour);
345 }
346
347 public function get_expected_data() {
348 return array();
349 }
350
351 public function get_correct_response() {
352 return array();
353 }
354
355 public function get_question_summary() {
356 return null;
357 }
358}
359
360
361/**
362 * Interface that a {@link question_definition} must implement to be usable by
363 * the manual graded behaviour.
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 */
368interface 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()}.
f7970e3c 375 * @return bool whether this response is a complete answer to this question.
d1b7e03d
TH
376 */
377 public function is_complete_response(array $response);
378
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.
f7970e3c 387 * @return bool whether the two sets of responses are the same - that is
d1b7e03d
TH
388 * whether the new set of responses can safely be discarded.
389 */
390 public function is_same_response(array $prevresponse, array $newresponse);
391
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);
398
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);
407}
408
409
410/**
411 * This class is used in the return value from
412 * {@link question_manually_gradable::classify_response()}.
413 *
7764183a
TH
414 * @copyright 2010 The Open University
415 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
416 */
417class 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 }
439
440 public static function no_response() {
441 return new question_classified_response(null, null, null);
442 }
443}
444
445
446/**
447 * Interface that a {@link question_definition} must implement to be usable by
448 * the various automatic grading behaviours.
449 *
7764183a
TH
450 * @copyright 2009 The Open University
451 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
452 */
453interface 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()}.
f7970e3c 460 * @return bool whether this response can be graded.
d1b7e03d
TH
461 */
462 public function is_gradable_response(array $response);
463
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);
470
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);
478
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.
f7970e3c 484 * @param int $hintnumber Which hint to display. Indexed starting from 0
d1b7e03d
TH
485 * @param question_attempt $qa The question_attempt.
486 */
487 public function get_hint($hintnumber, question_attempt $qa);
488
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();
497}
498
499
500/**
501 * Interface that a {@link question_definition} must implement to be usable by
502 * the interactivecountback behaviour.
503 *
7764183a
TH
504 * @copyright 2010 The Open University
505 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
506 */
507interface 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.
f7970e3c 514 * @param int $totaltries The maximum number of tries allowed.
d1b7e03d
TH
515 * @return numeric the fraction that should be awarded for this
516 * sequence of response.
517 */
518 public function compute_final_grade($responses, $totaltries);
519}
520
521
522/**
523 * This class represents a real question. That is, one that is not a
524 * {@link question_information_item}.
525 *
7764183a
TH
526 * @copyright 2009 The Open University
527 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
528 */
529abstract class question_with_responses extends question_definition
530 implements question_manually_gradable {
c7df5006 531 public function classify_response(array $response) {
d1b7e03d
TH
532 return array();
533 }
534}
535
536
537/**
538 * This class represents a question that can be graded automatically.
539 *
7764183a
TH
540 * @copyright 2009 The Open University
541 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
542 */
543abstract 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;
547
548 public function is_gradable_response(array $response) {
549 return $this->is_complete_response($response);
550 }
551
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 }
559
93cadb1e
TH
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.
f7970e3c 565 * @return bool whether access to the file should be allowed.
93cadb1e
TH
566 */
567 protected function check_combined_feedback_file_access($qa, $options, $filearea) {
568 $state = $qa->get_state();
569
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 }
577
578 return $options->feedback && $state->get_feedback_class() . 'feedback' == $filearea;
579 }
580
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.
f7970e3c 586 * @return bool whether access to the file should be allowed.
93cadb1e
TH
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 }
596
d1b7e03d
TH
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 }
7a719748
TH
603
604 public function format_hint(question_hint $hint, question_attempt $qa) {
22cebed5 605 return $this->format_text($hint->hint, $hint->hintformat, $qa, 'question', 'hint', $hint->id);
7a719748 606 }
d1b7e03d
TH
607}
608
609
610/**
611 * This class represents a question that can be graded automatically with
612 * countback grading in interactive mode.
613 *
7764183a
TH
614 * @copyright 2010 The Open University
615 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
616 */
617abstract class question_graded_automatically_with_countback
618 extends question_graded_automatically
619 implements question_automatically_gradable_with_countback {
620
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 }
627}
628
629
630/**
631 * This class represents a question that can be graded automatically by using
632 * a {@link question_grading_strategy}.
633 *
7764183a
TH
634 * @copyright 2009 The Open University
635 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
636 */
637abstract class question_graded_by_strategy extends question_graded_automatically {
638 /** @var question_grading_strategy the strategy to use for grading. */
639 protected $gradingstrategy;
640
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 }
646
647 public function get_correct_response() {
648 $answer = $this->get_correct_answer();
649 if (!$answer) {
650 return array();
651 }
652
653 return array('answer' => $answer->answer);
654 }
655
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 }
665
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 }
673
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 }
682
683 public function classify_response(array $response) {
684 if (empty($response['answer'])) {
685 return array($this->id => question_classified_response::no_response());
686 }
687
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 }
695}
696
697
698/**
699 * Class to represent a question answer, loaded from the question_answers table
700 * in the database.
701 *
7764183a
TH
702 * @copyright 2009 The Open University
703 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
704 */
705class question_answer {
5f7cfba7
TH
706 /** @var integer the answer id. */
707 public $id;
708
d1b7e03d
TH
709 /** @var string the answer. */
710 public $answer;
711
22cebed5
TH
712 /** @var integer one of the FORMAT_... constans. */
713 public $answerformat = FORMAT_PLAIN;
714
d1b7e03d
TH
715 /** @var number the fraction this answer is worth. */
716 public $fraction;
717
718 /** @var string the feedback for this answer. */
719 public $feedback;
720
5f7cfba7
TH
721 /** @var integer one of the FORMAT_... constans. */
722 public $feedbackformat;
723
d1b7e03d
TH
724 /**
725 * Constructor.
22cebed5 726 * @param int $id the answer.
d1b7e03d 727 * @param string $answer the answer.
22cebed5 728 * @param int $answerformat the format of the answer.
d1b7e03d
TH
729 * @param number $fraction the fraction this answer is worth.
730 * @param string $feedback the feedback for this answer.
22cebed5 731 * @param int $feedbackformat the format of the feedback.
d1b7e03d 732 */
5f7cfba7
TH
733 public function __construct($id, $answer, $fraction, $feedback, $feedbackformat) {
734 $this->id = $id;
d1b7e03d
TH
735 $this->answer = $answer;
736 $this->fraction = $fraction;
737 $this->feedback = $feedback;
5f7cfba7 738 $this->feedbackformat = $feedbackformat;
d1b7e03d
TH
739 }
740}
741
742
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 *
7764183a
TH
747 * @copyright 2010 The Open University
748 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
749 */
750class question_hint {
7a719748
TH
751 /** @var integer The hint id. */
752 public $id;
753 /** @var string The feedback hint to be shown. */
d1b7e03d 754 public $hint;
7a719748
TH
755 /** @var integer The corresponding text FORMAT_... type. */
756 public $hintformat;
d1b7e03d
TH
757
758 /**
759 * Constructor.
f7970e3c 760 * @param int the hint id from the database.
d1b7e03d 761 * @param string $hint The hint text
f7970e3c 762 * @param int the corresponding text FORMAT_... type.
d1b7e03d 763 */
7a719748
TH
764 public function __construct($id, $hint, $hintformat) {
765 $this->id = $id;
d1b7e03d 766 $this->hint = $hint;
7a719748 767 $this->hintformat = $hintformat;
d1b7e03d
TH
768 }
769
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) {
7a719748 776 return new question_hint($row->id, $row->hint, $row->hintformat);
d1b7e03d
TH
777 }
778
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 }
786}
787
788
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 *
7764183a
TH
794 * @copyright 2010 The Open University
795 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
796 */
797class 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;
800
801 /** @var boolean option to clear the parts of the question that were wrong on retry. */
802 public $clearwrong;
803
804 /**
805 * Constructor.
f7970e3c 806 * @param int the hint id from the database.
d1b7e03d 807 * @param string $hint The hint text
f7970e3c
TH
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.
d1b7e03d 811 */
7a719748
TH
812 public function __construct($id, $hint, $hintformat, $shownumcorrect, $clearwrong) {
813 parent::__construct($id, $hint, $hintformat);
d1b7e03d
TH
814 $this->shownumcorrect = $shownumcorrect;
815 $this->clearwrong = $clearwrong;
816 }
817
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) {
7a719748
TH
824 return new question_hint_with_parts($row->id, $row->hint, $row->hintformat,
825 $row->shownumcorrect, $row->clearwrong);
d1b7e03d
TH
826 }
827
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 }
835}
836
837
838/**
839 * This question_grading_strategy interface. Used to share grading code between
840 * questions that that subclass {@link question_graded_by_strategy}.
841 *
7764183a
TH
842 * @copyright 2009 The Open University
843 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
844 */
845interface 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);
853
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();
859}
860
861
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 *
7764183a
TH
867 * @copyright 2009 The Open University
868 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
869 */
870interface question_response_answer_comparer {
871 /** @return array of {@link question_answers}. */
872 public function get_answers();
873
874 /**
875 * @param array $response the response.
876 * @param question_answer $answer an answer.
f7970e3c 877 * @return bool whether the response matches the answer.
d1b7e03d
TH
878 */
879 public function compare_response_with_answer(array $response, question_answer $answer);
880}
881
882
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 *
7764183a
TH
889 * @copyright 2009 The Open University
890 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
891 */
892class 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;
898
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 }
906
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 }
916
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 }
926}