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