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