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