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