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