weekly release 2.2dev
[moodle.git] / question / type / questiontypebase.php
CommitLineData
f29aeb5a 1<?php
f29aeb5a
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
516cf3eb 17/**
4323d029 18 * The default questiontype class.
19 *
7764183a 20 * @package moodlecore
4323d029 21 * @subpackage questiontypes
7764183a
TH
22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
271e6dec 24 */
516cf3eb 25
f29aeb5a 26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
d1b7e03d 29require_once($CFG->dirroot . '/question/engine/lib.php');
516cf3eb 30
f29aeb5a 31
32b0adfa 32/**
33 * This is the base class for Moodle question types.
271ffe3f 34 *
32b0adfa 35 * There are detailed comments on each method, explaining what the method is
36 * for, and the circumstances under which you might need to override it.
271ffe3f 37 *
32b0adfa 38 * Note: the questiontype API should NOT be considered stable yet. Very few
22cebed5 39 * question types have been produced yet, so we do not yet know all the places
271ffe3f 40 * where the current API is insufficient. I would rather learn from the
32b0adfa 41 * experiences of the first few question type implementors, and improve the
42 * interface to meet their needs, rather the freeze the API prematurely and
43 * condem everyone to working round a clunky interface for ever afterwards.
271e6dec 44 *
7764183a
TH
45 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32b0adfa 47 */
d1b7e03d 48class question_type {
f29aeb5a
TH
49 protected $fileoptions = array(
50 'subdirs' => false,
51 'maxfiles' => -1,
52 'maxbytes' => 0,
53 );
516cf3eb 54
d1b7e03d 55 public function __construct() {
869309b8 56 }
57
4995b9c1 58 /**
d1b7e03d 59 * @return string the name of this question type.
869309b8 60 */
d1b7e03d
TH
61 public function name() {
62 return substr(get_class($this), 6);
a2156789 63 }
271ffe3f 64
f29aeb5a
TH
65 /**
66 * @return string the full frankenstyle name for this plugin.
67 */
68 public function plugin_name() {
69 return get_class($this);
70 }
71
72 /**
73 * @return string the name of this question type in the user's language.
74 * You should not need to override this method, the default behaviour should be fine.
75 */
76 public function local_name() {
77 return get_string($this->name(), $this->plugin_name());
78 }
79
869309b8 80 /**
81 * The name this question should appear as in the create new question
f29aeb5a
TH
82 * dropdown. Override this method to return false if you don't want your
83 * question type to be createable, for example if it is an abstract base type,
84 * otherwise, you should not need to override this method.
869309b8 85 *
86 * @return mixed the desired string, or false to hide this question type in the menu.
87 */
d1b7e03d 88 public function menu_name() {
f29aeb5a
TH
89 return $this->local_name();
90 }
91
92 /**
93 * Returns a list of other question types that this one requires in order to
94 * work. For example, the calculated question type is a subclass of the
95 * numerical question type, which is a subclass of the shortanswer question
96 * type; and the randomsamatch question type requires the shortanswer type
97 * to be installed.
98 *
99 * @return array any other question types that this one relies on. An empty
100 * array if none.
101 */
102 public function requires_qtypes() {
103 return array();
104 }
105
106 /**
f7970e3c 107 * @return bool override this to return false if this is not really a
f29aeb5a
TH
108 * question type, for example the description question type is not
109 * really a question type.
110 */
111 public function is_real_question_type() {
112 return true;
b974f947 113 }
114
a2156789 115 /**
f7970e3c 116 * @return bool true if this question type sometimes requires manual grading.
a2156789 117 */
d1b7e03d 118 public function is_manual_graded() {
a2156789 119 return false;
120 }
121
f24493ec 122 /**
123 * @param object $question a question of this type.
124 * @param string $otherquestionsinuse comma-separate list of other question ids in this attempt.
f7970e3c 125 * @return bool true if a particular instance of this question requires manual grading.
f24493ec 126 */
d1b7e03d 127 public function is_question_manual_graded($question, $otherquestionsinuse) {
f24493ec 128 return $this->is_manual_graded();
129 }
130
869309b8 131 /**
f7970e3c 132 * @return bool true if this question type can be used by the random question type.
869309b8 133 */
d1b7e03d
TH
134 public function is_usable_by_random() {
135 return true;
869309b8 136 }
137
a2156789 138 /**
d1b7e03d
TH
139 * Whether this question type can perform a frequency analysis of student
140 * responses.
141 *
142 * If this method returns true, you must implement the get_possible_responses
143 * method, and the question_definition class must implement the
144 * classify_response method.
145 *
f7970e3c 146 * @return bool whether this report can analyse all the student reponses
d1b7e03d 147 * for things like the quiz statistics report.
a2156789 148 */
d1b7e03d
TH
149 public function can_analyse_responses() {
150 // This works in most cases.
151 return !$this->is_manual_graded();
a2156789 152 }
153
869309b8 154 /**
d1b7e03d
TH
155 * @return whether the question_answers.answer field needs to have
156 * restore_decode_content_links_worker called on it.
869309b8 157 */
d1b7e03d 158 public function has_html_answers() {
869309b8 159 return false;
160 }
d001dac7 161
295043c2 162 /**
163 * If your question type has a table that extends the question table, and
164 * you want the base class to automatically save, backup and restore the extra fields,
165 * override this method to return an array wherer the first element is the table name,
166 * and the subsequent entries are the column names (apart from id and questionid).
167 *
168 * @return mixed array as above, or null to tell the base class to do nothing.
169 */
d1b7e03d 170 public function extra_question_fields() {
295043c2 171 return null;
172 }
173
d001dac7 174 /**
eaeb6b51
TH
175 * If you use extra_question_fields, overload this function to return question id field name
176 * in case you table use another name for this column
177 */
d1b7e03d 178 protected function questionid_column_name() {
d001dac7 179 return 'questionid';
180 }
181
295043c2 182 /**
183 * If your question type has a table that extends the question_answers table,
184 * make this method return an array wherer the first element is the table name,
185 * and the subsequent entries are the column names (apart from id and answerid).
186 *
187 * @return mixed array as above, or null to tell the base class to do nothing.
188 */
6b5f24d3
TH
189 protected function extra_answer_fields() {
190 return null;
191 }
192
193 /**
194 * If the quetsion type uses files in responses, then this method should
195 * return an array of all the response variables that might have corresponding
196 * files. For example, the essay qtype returns array('attachments', 'answers').
197 *
198 * @return array response variable names that may have associated files.
199 */
200 public function response_file_areas() {
201 return array();
202 }
295043c2 203
36703ed7 204 /**
205 * Return an instance of the question editing form definition. This looks for a
206 * class called edit_{$this->name()}_question_form in the file
f29aeb5a 207 * question/type/{$this->name()}/edit_{$this->name()}_question_form.php
36703ed7 208 * and if it exists returns an instance of it.
209 *
210 * @param string $submiturl passed on to the constructor call.
211 * @return object an instance of the form definition, or null if one could not be found.
212 */
eaeb6b51
TH
213 public function create_editing_form($submiturl, $question, $category,
214 $contexts, $formeditable) {
36703ed7 215 global $CFG;
1fa39364
TH
216 require_once($CFG->dirroot . '/question/type/edit_question_form.php');
217 $definitionfile = $CFG->dirroot . '/question/type/' . $this->name() .
eaeb6b51 218 '/edit_' . $this->name() . '_form.php';
1fa39364
TH
219 if (!is_readable($definitionfile) || !is_file($definitionfile)) {
220 throw new coding_exception($this->plugin_name() .
afe24f85
TH
221 ' is missing the definition of its editing formin file ' .
222 $definitionfile . '.');
36703ed7 223 }
1fa39364 224 require_once($definitionfile);
6e34cd29 225 $classname = $this->plugin_name() . '_edit_form';
36703ed7 226 if (!class_exists($classname)) {
1fa39364
TH
227 throw new coding_exception($this->plugin_name() .
228 ' does not define the class ' . $this->plugin_name() .
229 '_edit_form.');
36703ed7 230 }
271e6dec 231 return new $classname($submiturl, $question, $category, $contexts, $formeditable);
36703ed7 232 }
233
99ba746d 234 /**
235 * @return string the full path of the folder this plugin's files live in.
236 */
d1b7e03d 237 public function plugin_dir() {
99ba746d 238 global $CFG;
239 return $CFG->dirroot . '/question/type/' . $this->name();
240 }
241
242 /**
243 * @return string the URL of the folder this plugin's files live in.
244 */
d1b7e03d 245 public function plugin_baseurl() {
99ba746d 246 global $CFG;
247 return $CFG->wwwroot . '/question/type/' . $this->name();
248 }
249
7b41a4a9 250 /**
251 * This method should be overriden if you want to include a special heading or some other
252 * html on a question editing page besides the question editing form.
253 *
254 * @param question_edit_form $mform a child of question_edit_form
255 * @param object $question
256 * @param string $wizardnow is '' for first page.
257 */
f29aeb5a
TH
258 public function display_question_editing_page($mform, $question, $wizardnow) {
259 global $OUTPUT;
260 $heading = $this->get_heading(empty($question->id));
261
262 echo $OUTPUT->heading_with_help($heading, $this->name(), $this->plugin_name());
263
271e6dec 264 $permissionstrs = array();
eaeb6b51
TH
265 if (!empty($question->id)) {
266 if ($question->formoptions->canedit) {
271e6dec 267 $permissionstrs[] = get_string('permissionedit', 'question');
268 }
eaeb6b51 269 if ($question->formoptions->canmove) {
271e6dec 270 $permissionstrs[] = get_string('permissionmove', 'question');
271 }
eaeb6b51 272 if ($question->formoptions->cansaveasnew) {
271e6dec 273 $permissionstrs[] = get_string('permissionsaveasnew', 'question');
274 }
275 }
eaeb6b51 276 if (!$question->formoptions->movecontext && count($permissionstrs)) {
f29aeb5a 277 echo $OUTPUT->heading(get_string('permissionto', 'question'), 3);
271e6dec 278 $html = '<ul>';
eaeb6b51 279 foreach ($permissionstrs as $permissionstr) {
271e6dec 280 $html .= '<li>'.$permissionstr.'</li>';
281 }
282 $html .= '</ul>';
f29aeb5a 283 echo $OUTPUT->box($html, 'boxwidthnarrow boxaligncenter generalbox');
271e6dec 284 }
9ab75b2b 285 $mform->display();
286 }
271e6dec 287
9ab75b2b 288 /**
eaeb6b51
TH
289 * Method called by display_question_editing_page and by question.php to get
290 * heading for breadcrumbs.
271e6dec 291 *
d1b7e03d 292 * @return string the heading
9ab75b2b 293 */
eaeb6b51 294 public function get_heading($adding = false) {
f29aeb5a 295 if ($adding) {
d1b7e03d 296 $action = 'adding';
271e6dec 297 } else {
d1b7e03d 298 $action = 'editing';
c2f8c4be 299 }
f29aeb5a 300 return get_string($action . $this->name(), $this->plugin_name());
d1b7e03d
TH
301 }
302
303 /**
304 * Set any missing settings for this question to the default values. This is
305 * called before displaying the question editing form.
306 *
307 * @param object $questiondata the question data, loaded from the databsae,
308 * or more likely a newly created question object that is only partially
309 * initialised.
310 */
311 public function set_default_options($questiondata) {
7b41a4a9 312 }
313
516cf3eb 314 /**
eaeb6b51
TH
315 * Saves (creates or updates) a question.
316 *
317 * Given some question info and some data about the answers
318 * this function parses, organises and saves the question
319 * It is used by {@link question.php} when saving new data from
320 * a form, and also by {@link import.php} when importing questions
321 * This function in turn calls {@link save_question_options}
322 * to save question-type specific data.
323 *
324 * Whether we are saving a new question or updating an existing one can be
325 * determined by testing !empty($question->id). If it is not empty, we are updating.
326 *
327 * The question will be saved in category $form->category.
328 *
329 * @param object $question the question object which should be updated. For a
330 * new question will be mostly empty.
331 * @param object $form the object containing the information to save, as if
332 * from the question editing form.
333 * @param object $course not really used any more.
334 * @return object On success, return the new question object. On failure,
335 * return an object as follows. If the error object has an errors field,
336 * display that as an error message. Otherwise, the editing form will be
337 * redisplayed with validation errors, from validation_errors field, which
338 * is itself an object, shown next to the form fields. (I don't think this
339 * is accurate any more.)
340 */
c7df5006 341 public function save_question($question, $form) {
f29aeb5a
TH
342 global $USER, $DB, $OUTPUT;
343
344 list($question->category) = explode(',', $form->category);
345 $context = $this->get_context_by_category_id($question->category);
fe6ce234 346
516cf3eb 347 // This default implementation is suitable for most
348 // question types.
271ffe3f 349
516cf3eb 350 // First, save the basic question itself
24e8b9b6 351 $question->name = trim($form->name);
24e8b9b6 352 $question->parent = isset($form->parent) ? $form->parent : 0;
516cf3eb 353 $question->length = $this->actual_number_of_questions($question);
354 $question->penalty = isset($form->penalty) ? $form->penalty : 0;
355
f29aeb5a
TH
356 if (empty($form->questiontext['text'])) {
357 $question->questiontext = '';
516cf3eb 358 } else {
f29aeb5a 359 $question->questiontext = trim($form->questiontext['text']);;
516cf3eb 360 }
eaeb6b51
TH
361 $question->questiontextformat = !empty($form->questiontext['format']) ?
362 $form->questiontext['format'] : 0;
516cf3eb 363
f29aeb5a 364 if (empty($form->generalfeedback['text'])) {
a4514d91 365 $question->generalfeedback = '';
1b8a7434 366 } else {
f29aeb5a 367 $question->generalfeedback = trim($form->generalfeedback['text']);
1b8a7434 368 }
eaeb6b51
TH
369 $question->generalfeedbackformat = !empty($form->generalfeedback['format']) ?
370 $form->generalfeedback['format'] : 0;
1b8a7434 371
516cf3eb 372 if (empty($question->name)) {
f29aeb5a 373 $question->name = shorten_text(strip_tags($form->questiontext['text']), 15);
516cf3eb 374 if (empty($question->name)) {
375 $question->name = '-';
376 }
377 }
378
379 if ($question->penalty > 1 or $question->penalty < 0) {
5e8a85aa 380 $question->errors['penalty'] = get_string('invalidpenalty', 'question');
516cf3eb 381 }
382
d1b7e03d
TH
383 if (isset($form->defaultmark)) {
384 $question->defaultmark = $form->defaultmark;
516cf3eb 385 }
386
f29aeb5a
TH
387 // If the question is new, create it.
388 if (empty($question->id)) {
cbe20043 389 // Set the unique code
390 $question->stamp = make_unique_id_code();
271e6dec 391 $question->createdby = $USER->id;
392 $question->timecreated = time();
f29aeb5a 393 $question->id = $DB->insert_record('question', $question);
516cf3eb 394 }
395
f29aeb5a
TH
396 // Now, whether we are updating a existing question, or creating a new
397 // one, we have to do the files processing and update the record.
398 /// Question already exists, update.
399 $question->modifiedby = $USER->id;
400 $question->timemodified = time();
401
402 if (!empty($question->questiontext) && !empty($form->questiontext['itemid'])) {
eaeb6b51
TH
403 $question->questiontext = file_save_draft_area_files($form->questiontext['itemid'],
404 $context->id, 'question', 'questiontext', (int)$question->id,
405 $this->fileoptions, $question->questiontext);
f29aeb5a
TH
406 }
407 if (!empty($question->generalfeedback) && !empty($form->generalfeedback['itemid'])) {
eaeb6b51
TH
408 $question->generalfeedback = file_save_draft_area_files(
409 $form->generalfeedback['itemid'], $context->id,
410 'question', 'generalfeedback', (int)$question->id,
411 $this->fileoptions, $question->generalfeedback);
f29aeb5a
TH
412 }
413 $DB->update_record('question', $question);
414
516cf3eb 415 // Now to save all the answers and type-specific options
6459b8fc 416 $form->id = $question->id;
417 $form->qtype = $question->qtype;
516cf3eb 418 $form->category = $question->category;
6459b8fc 419 $form->questiontext = $question->questiontext;
f29aeb5a
TH
420 $form->questiontextformat = $question->questiontextformat;
421 // current context
422 $form->context = $context;
516cf3eb 423
424 $result = $this->save_question_options($form);
425
426 if (!empty($result->error)) {
f29aeb5a 427 print_error($result->error);
516cf3eb 428 }
429
430 if (!empty($result->notice)) {
431 notice($result->notice, "question.php?id=$question->id");
432 }
433
434 if (!empty($result->noticeyesno)) {
eaeb6b51
TH
435 throw new coding_exception(
436 '$result->noticeyesno no longer supported in save_question.');
516cf3eb 437 }
438
cbe20043 439 // Give the question a unique version stamp determined by question_hash()
eaeb6b51
TH
440 $DB->set_field('question', 'version', question_hash($question),
441 array('id' => $question->id));
cbe20043 442
516cf3eb 443 return $question;
444 }
271ffe3f 445
516cf3eb 446 /**
d1b7e03d
TH
447 * Saves question-type specific options
448 *
449 * This is called by {@link save_question()} to save the question-type specific data
450 * @return object $result->error or $result->noticeyesno or $result->notice
451 * @param object $question This holds the information from the editing form,
452 * it is not a standard question object.
453 */
454 public function save_question_options($question) {
f29aeb5a 455 global $DB;
eaeb6b51 456 $extraquestionfields = $this->extra_question_fields();
295043c2 457
eaeb6b51
TH
458 if (is_array($extraquestionfields)) {
459 $question_extension_table = array_shift($extraquestionfields);
271e6dec 460
295043c2 461 $function = 'update_record';
d001dac7 462 $questionidcolname = $this->questionid_column_name();
eaeb6b51
TH
463 $options = $DB->get_record($question_extension_table,
464 array($questionidcolname => $question->id));
295043c2 465 if (!$options) {
466 $function = 'insert_record';
0ff4bd08 467 $options = new stdClass();
d001dac7 468 $options->$questionidcolname = $question->id;
295043c2 469 }
eaeb6b51 470 foreach ($extraquestionfields as $field) {
295043c2 471 if (!isset($question->$field)) {
0ff4bd08 472 $result = new stdClass();
295043c2 473 $result->error = "No data for field $field when saving " .
474 $this->name() . ' question id ' . $question->id;
475 return $result;
476 }
477 $options->$field = $question->$field;
478 }
271e6dec 479
f29aeb5a 480 if (!$DB->{$function}($question_extension_table, $options)) {
0ff4bd08 481 $result = new stdClass();
295043c2 482 $result->error = 'Could not save question options for ' .
483 $this->name() . ' question id ' . $question->id;
484 return $result;
485 }
486 }
487
eaeb6b51 488 $extraanswerfields = $this->extra_answer_fields();
295043c2 489 // TODO save the answers, with any extra data.
516cf3eb 490 }
491
d1b7e03d 492 public function save_hints($formdata, $withparts = false) {
f29aeb5a 493 global $DB;
7a719748
TH
494 $context = $formdata->context;
495
e5060e22
JP
496 $oldhints = $DB->get_records('question_hints',
497 array('questionid' => $formdata->id), 'id ASC');
295043c2 498
d1b7e03d
TH
499 if (!empty($formdata->hint)) {
500 $numhints = max(array_keys($formdata->hint)) + 1;
501 } else {
502 $numhints = 0;
516cf3eb 503 }
295043c2 504
d1b7e03d
TH
505 if ($withparts) {
506 if (!empty($formdata->hintclearwrong)) {
507 $numclears = max(array_keys($formdata->hintclearwrong)) + 1;
508 } else {
509 $numclears = 0;
510 }
511 if (!empty($formdata->hintshownumcorrect)) {
512 $numshows = max(array_keys($formdata->hintshownumcorrect)) + 1;
513 } else {
514 $numshows = 0;
515 }
516 $numhints = max($numhints, $numclears, $numshows);
517 }
518
519 for ($i = 0; $i < $numhints; $i += 1) {
7a719748
TH
520 if (html_is_blank($formdata->hint[$i]['text'])) {
521 $formdata->hint[$i]['text'] = '';
d1b7e03d
TH
522 }
523
524 if ($withparts) {
7a719748
TH
525 $clearwrong = !empty($formdata->hintclearwrong[$i]);
526 $shownumcorrect = !empty($formdata->hintshownumcorrect[$i]);
d1b7e03d
TH
527 }
528
eaeb6b51
TH
529 if (empty($formdata->hint[$i]['text']) && empty($clearwrong) &&
530 empty($shownumcorrect)) {
d1b7e03d
TH
531 continue;
532 }
533
f3ca24e4 534 // Update an existing hint if possible.
7a719748
TH
535 $hint = array_shift($oldhints);
536 if (!$hint) {
537 $hint = new stdClass();
538 $hint->questionid = $formdata->id;
539 $hint->hint = '';
540 $hint->id = $DB->insert_record('question_hints', $hint);
541 }
542
7a719748
TH
543 $hint->hint = $this->import_or_save_files($formdata->hint[$i],
544 $context, 'question', 'hint', $hint->id);
545 $hint->hintformat = $formdata->hint[$i]['format'];
546 if ($withparts) {
547 $hint->clearwrong = $clearwrong;
548 $hint->shownumcorrect = $shownumcorrect;
549 }
550 $DB->update_record('question_hints', $hint);
551 }
552
553 // Delete any remaining old hints.
554 $fs = get_file_storage();
c73c9836 555 foreach ($oldhints as $oldhint) {
7a719748
TH
556 $fs->delete_area_files($context->id, 'question', 'hint', $oldhint->id);
557 $DB->delete_records('question_hints', array('id' => $oldhint->id));
d1b7e03d
TH
558 }
559 }
560
1c2ed7c5
TH
561 /**
562 * Can be used to {@link save_question_options()} to transfer the combined
563 * feedback fields from $formdata to $options.
564 * @param object $options the $question->options object being built.
565 * @param object $formdata the data from the form.
566 * @param object $context the context the quetsion is being saved into.
f7970e3c 567 * @param bool $withparts whether $options->shownumcorrect should be set.
1c2ed7c5 568 */
eaeb6b51
TH
569 protected function save_combined_feedback_helper($options, $formdata,
570 $context, $withparts = false) {
1c2ed7c5
TH
571 $options->correctfeedback = $this->import_or_save_files($formdata->correctfeedback,
572 $context, 'question', 'correctfeedback', $formdata->id);
573 $options->correctfeedbackformat = $formdata->correctfeedback['format'];
eaeb6b51
TH
574
575 $options->partiallycorrectfeedback = $this->import_or_save_files(
576 $formdata->partiallycorrectfeedback,
1c2ed7c5
TH
577 $context, 'question', 'partiallycorrectfeedback', $formdata->id);
578 $options->partiallycorrectfeedbackformat = $formdata->partiallycorrectfeedback['format'];
eaeb6b51 579
1c2ed7c5
TH
580 $options->incorrectfeedback = $this->import_or_save_files($formdata->incorrectfeedback,
581 $context, 'question', 'incorrectfeedback', $formdata->id);
582 $options->incorrectfeedbackformat = $formdata->incorrectfeedback['format'];
583
584 if ($withparts) {
585 $options->shownumcorrect = !empty($formdata->shownumcorrect);
586 }
587
588 return $options;
589 }
590
d1b7e03d
TH
591 /**
592 * Loads the question type specific options for the question.
593 *
594 * This function loads any question type specific options for the
595 * question from the database into the question object. This information
596 * is placed in the $question->options field. A question type is
597 * free, however, to decide on a internal structure of the options field.
598 * @return bool Indicates success or failure.
599 * @param object $question The question object for the question. This object
600 * should be updated to include the question type
601 * specific information (it is passed by reference).
602 */
603 public function get_question_options($question) {
f29aeb5a
TH
604 global $CFG, $DB, $OUTPUT;
605
606 if (!isset($question->options)) {
607 $question->options = new stdClass();
608 }
d1b7e03d 609
eaeb6b51
TH
610 $extraquestionfields = $this->extra_question_fields();
611 if (is_array($extraquestionfields)) {
612 $question_extension_table = array_shift($extraquestionfields);
613 $extra_data = $DB->get_record($question_extension_table,
614 array($this->questionid_column_name() => $question->id),
615 implode(', ', $extraquestionfields));
295043c2 616 if ($extra_data) {
eaeb6b51 617 foreach ($extraquestionfields as $field) {
295043c2 618 $question->options->$field = $extra_data->$field;
619 }
620 } else {
eaeb6b51
TH
621 echo $OUTPUT->notification('Failed to load question options from the table ' .
622 $question_extension_table . ' for questionid ' . $question->id);
295043c2 623 return false;
624 }
625 }
626
eaeb6b51
TH
627 $extraanswerfields = $this->extra_answer_fields();
628 if (is_array($extraanswerfields)) {
629 $answer_extension_table = array_shift($extraanswerfields);
f29aeb5a 630 $question->options->answers = $DB->get_records_sql("
eaeb6b51 631 SELECT qa.*, qax." . implode(', qax.', $extraanswerfields) . "
e5060e22 632 FROM {question_answers} qa, {{$answer_extension_table}} qax
da8cd9f4
JP
633 WHERE qa.question = ? AND qax.answerid = qa.id
634 ORDER BY qa.id", array($question->id));
295043c2 635 if (!$question->options->answers) {
eaeb6b51
TH
636 echo $OUTPUT->notification('Failed to load question answers from the table ' .
637 $answer_extension_table . 'for questionid ' . $question->id);
295043c2 638 return false;
639 }
640 } else {
eaeb6b51
TH
641 // Don't check for success or failure because some question types do
642 // not use the answers table.
643 $question->options->answers = $DB->get_records('question_answers',
644 array('question' => $question->id), 'id ASC');
295043c2 645 }
646
eaeb6b51
TH
647 $question->hints = $DB->get_records('question_hints',
648 array('questionid' => $question->id), 'id ASC');
d1b7e03d 649
516cf3eb 650 return true;
651 }
652
653 /**
d1b7e03d
TH
654 * Create an appropriate question_definition for the question of this type
655 * using data loaded from the database.
656 * @param object $questiondata the question data loaded from the database.
657 * @return question_definition the corresponding question_definition.
658 */
659 public function make_question($questiondata) {
660 $question = $this->make_question_instance($questiondata);
661 $this->initialise_question_instance($question, $questiondata);
662 return $question;
663 }
0429cd86 664
d1b7e03d
TH
665 /**
666 * Create an appropriate question_definition for the question of this type
667 * using data loaded from the database.
668 * @param object $questiondata the question data loaded from the database.
669 * @return question_definition an instance of the appropriate question_definition subclass.
670 * Still needs to be initialised.
671 */
672 protected function make_question_instance($questiondata) {
673 question_bank::load_question_definition_classes($this->name());
674 $class = 'qtype_' . $this->name() . '_question';
675 return new $class();
676 }
677
678 /**
679 * Initialise the common question_definition fields.
680 * @param question_definition $question the question_definition we are creating.
681 * @param object $questiondata the question data loaded from the database.
682 */
683 protected function initialise_question_instance(question_definition $question, $questiondata) {
684 $question->id = $questiondata->id;
685 $question->category = $questiondata->category;
56e82d99 686 $question->contextid = $questiondata->contextid;
d1b7e03d
TH
687 $question->parent = $questiondata->parent;
688 $question->qtype = $this;
689 $question->name = $questiondata->name;
690 $question->questiontext = $questiondata->questiontext;
691 $question->questiontextformat = $questiondata->questiontextformat;
692 $question->generalfeedback = $questiondata->generalfeedback;
1c2ed7c5 693 $question->generalfeedbackformat = $questiondata->generalfeedbackformat;
d1b7e03d
TH
694 $question->defaultmark = $questiondata->defaultmark + 0;
695 $question->length = $questiondata->length;
696 $question->penalty = $questiondata->penalty;
697 $question->stamp = $questiondata->stamp;
698 $question->version = $questiondata->version;
699 $question->hidden = $questiondata->hidden;
700 $question->timecreated = $questiondata->timecreated;
701 $question->timemodified = $questiondata->timemodified;
702 $question->createdby = $questiondata->createdby;
703 $question->modifiedby = $questiondata->modifiedby;
704
705 $this->initialise_question_hints($question, $questiondata);
706 }
707
708 /**
709 * Initialise question_definition::hints field.
710 * @param question_definition $question the question_definition we are creating.
711 * @param object $questiondata the question data loaded from the database.
712 */
713 protected function initialise_question_hints(question_definition $question, $questiondata) {
714 if (empty($questiondata->hints)) {
715 return;
716 }
717 foreach ($questiondata->hints as $hint) {
718 $question->hints[] = $this->make_hint($hint);
719 }
0429cd86 720 }
721
722 /**
d1b7e03d
TH
723 * Create a question_hint, or an appropriate subclass for this question,
724 * from a row loaded from the database.
725 * @param object $hint the DB row from the question hints table.
726 * @return question_hint
9203b705 727 */
d1b7e03d
TH
728 protected function make_hint($hint) {
729 return question_hint::load_from_record($hint);
730 }
731
1c2ed7c5
TH
732 /**
733 * Initialise the combined feedback fields.
734 * @param question_definition $question the question_definition we are creating.
735 * @param object $questiondata the question data loaded from the database.
f7970e3c 736 * @param bool $withparts whether to set the shownumcorrect field.
1c2ed7c5 737 */
eaeb6b51
TH
738 protected function initialise_combined_feedback(question_definition $question,
739 $questiondata, $withparts = false) {
1c2ed7c5
TH
740 $question->correctfeedback = $questiondata->options->correctfeedback;
741 $question->correctfeedbackformat = $questiondata->options->correctfeedbackformat;
742 $question->partiallycorrectfeedback = $questiondata->options->partiallycorrectfeedback;
eaeb6b51
TH
743 $question->partiallycorrectfeedbackformat =
744 $questiondata->options->partiallycorrectfeedbackformat;
1c2ed7c5
TH
745 $question->incorrectfeedback = $questiondata->options->incorrectfeedback;
746 $question->incorrectfeedbackformat = $questiondata->options->incorrectfeedbackformat;
747 if ($withparts) {
748 $question->shownumcorrect = $questiondata->options->shownumcorrect;
749 }
750 }
751
d1b7e03d
TH
752 /**
753 * Initialise question_definition::answers field.
754 * @param question_definition $question the question_definition we are creating.
755 * @param object $questiondata the question data loaded from the database.
35c9b652
TH
756 * @param bool $forceplaintextanswers most qtypes assume that answers are
757 * FORMAT_PLAIN, and dont use the answerformat DB column (it contains
758 * the default 0 = FORMAT_MOODLE). Therefore, by default this method
759 * ingores answerformat. Pass false here to use answerformat. For example
760 * multichoice does this.
761 */
762 protected function initialise_question_answers(question_definition $question,
763 $questiondata, $forceplaintextanswers = true) {
d1b7e03d
TH
764 $question->answers = array();
765 if (empty($questiondata->options->answers)) {
766 return;
767 }
768 foreach ($questiondata->options->answers as $a) {
5f7cfba7
TH
769 $question->answers[$a->id] = new question_answer($a->id, $a->answer,
770 $a->fraction, $a->feedback, $a->feedbackformat);
35c9b652
TH
771 if (!$forceplaintextanswers) {
772 $question->answers[$a->id]->answerformat = $a->answerformat;
773 }
d1b7e03d
TH
774 }
775 }
9203b705 776
d1b7e03d 777 /**
f29aeb5a 778 * Deletes the question-type specific data when a question is deleted.
f7970e3c
TH
779 * @param int $question the question being deleted.
780 * @param int $contextid the context this quesiotn belongs to.
f29aeb5a
TH
781 */
782 public function delete_question($questionid, $contextid) {
783 global $DB;
784
785 $this->delete_files($questionid, $contextid);
516cf3eb 786
eaeb6b51
TH
787 $extraquestionfields = $this->extra_question_fields();
788 if (is_array($extraquestionfields)) {
789 $question_extension_table = array_shift($extraquestionfields);
f29aeb5a
TH
790 $DB->delete_records($question_extension_table,
791 array($this->questionid_column_name() => $questionid));
295043c2 792 }
793
eaeb6b51
TH
794 $extraanswerfields = $this->extra_answer_fields();
795 if (is_array($extraanswerfields)) {
796 $answer_extension_table = array_shift($extraanswerfields);
f29aeb5a 797 $DB->delete_records_select($answer_extension_table,
eaeb6b51
TH
798 'answerid IN (SELECT qa.id FROM {question_answers} qa WHERE qa.question = ?)',
799 array($questionid));
295043c2 800 }
801
f29aeb5a 802 $DB->delete_records('question_answers', array('question' => $questionid));
d1b7e03d 803
f29aeb5a 804 $DB->delete_records('question_hints', array('questionid' => $questionid));
516cf3eb 805 }
806
807 /**
eaeb6b51
TH
808 * Returns the number of question numbers which are used by the question
809 *
810 * This function returns the number of question numbers to be assigned
811 * to the question. Most question types will have length one; they will be
812 * assigned one number. The 'description' type, however does not use up a
813 * number and so has a length of zero. Other question types may wish to
814 * handle a bundle of questions and hence return a number greater than one.
815 * @return int The number of question numbers which should be
816 * assigned to the question.
817 * @param object $question The question whose length is to be determined.
818 * Question type specific information is included.
819 */
d1b7e03d 820 public function actual_number_of_questions($question) {
516cf3eb 821 // By default, each question is given one number
822 return 1;
823 }
824
6f51ed72 825 /**
826 * @param object $question
d1b7e03d
TH
827 * @return number|null either a fraction estimating what the student would
828 * score by guessing, or null, if it is not possible to estimate.
6f51ed72 829 */
c7df5006 830 public function get_random_guess_score($questiondata) {
6f51ed72 831 return 0;
832 }
516cf3eb 833
516cf3eb 834 /**
d1b7e03d 835 * This method should return all the possible types of response that are
f9b0500f 836 * recognised for this question.
271e6dec 837 *
d1b7e03d
TH
838 * The question is modelled as comprising one or more subparts. For each
839 * subpart, there are one or more classes that that students response
840 * might fall into, each of those classes earning a certain score.
50da63eb 841 *
d1b7e03d
TH
842 * For example, in a shortanswer question, there is only one subpart, the
843 * text entry field. The response the student gave will be classified according
844 * to which of the possible $question->options->answers it matches.
271e6dec 845 *
d1b7e03d
TH
846 * For the matching question type, there will be one subpart for each
847 * question stem, and for each stem, each of the possible choices is a class
848 * of student's response.
849 *
850 * A response is an object with two fields, ->responseclass is a string
851 * presentation of that response, and ->fraction, the credit for a response
852 * in that class.
853 *
854 * Array keys have no specific meaning, but must be unique, and must be
855 * the same if this function is called repeatedly.
856 *
857 * @param object $question the question definition data.
858 * @return array keys are subquestionid, values are arrays of possible
859 * responses to that subquestion.
99ba746d 860 */
c7df5006 861 public function get_possible_responses($questiondata) {
d1b7e03d 862 return array();
50da63eb 863 }
5fceb049 864
50da63eb 865 /**
f29aeb5a
TH
866 * Like @see{get_html_head_contributions}, but this method is for CSS and
867 * JavaScript required on the question editing page question/question.php.
50da63eb 868 */
d1b7e03d 869 public function get_editing_head_contributions() {
50da63eb 870 // By default, we link to any of the files styles.css, styles.php,
871 // script.js or script.php that exist in the plugin folder.
872 // Core question types should not use this mechanism. Their styles
873 // should be included in the standard theme.
f29aeb5a 874 $this->find_standard_scripts();
50da63eb 875 }
271e6dec 876
50da63eb 877 /**
f29aeb5a 878 * Utility method used by @see{get_html_head_contributions} and
50da63eb 879 * @see{get_editing_head_contributions}. This looks for any of the files
f29aeb5a
TH
880 * script.js or script.php that exist in the plugin folder and ensures they
881 * get included.
50da63eb 882 */
c76145d3 883 public function find_standard_scripts() {
f29aeb5a
TH
884 global $PAGE;
885
99ba746d 886 $plugindir = $this->plugin_dir();
f29aeb5a 887 $plugindirrel = 'question/type/' . $this->name();
50da63eb 888
889 if (file_exists($plugindir . '/script.js')) {
f29aeb5a 890 $PAGE->requires->js('/' . $plugindirrel . '/script.js');
50da63eb 891 }
892 if (file_exists($plugindir . '/script.php')) {
f29aeb5a 893 $PAGE->requires->js('/' . $plugindirrel . '/script.php');
516cf3eb 894 }
fe9b5cfd 895 }
896
62e76c67 897 /**
eaeb6b51
TH
898 * Returns true if the editing wizard is finished, false otherwise.
899 *
900 * The default implementation returns true, which is suitable for all question-
901 * types that only use one editing form. This function is used in
902 * question.php to decide whether we can regrade any states of the edited
903 * question and redirect to edit.php.
904 *
905 * The dataset dependent question-type, which is extended by the calculated
906 * question-type, overwrites this method because it uses multiple pages (i.e.
907 * a wizard) to set up the question and associated datasets.
908 *
909 * @param object $form The data submitted by the previous page.
910 *
911 * @return bool Whether the wizard's last page was submitted or not.
912 */
f29aeb5a 913 public function finished_edit_wizard($form) {
d1b7e03d
TH
914 //In the default case there is only one edit page.
915 return true;
62e76c67 916 }
917
eaeb6b51 918 /// IMPORT/EXPORT FUNCTIONS /////////////////
88bc20c3 919
920 /*
921 * Imports question from the Moodle XML format
922 *
923 * Imports question using information from extra_question_fields function
924 * If some of you fields contains id's you'll need to reimplement this
925 */
d1b7e03d 926 public function import_from_xml($data, $question, $format, $extra=null) {
88bc20c3 927 $question_type = $data['@']['type'];
928 if ($question_type != $this->name()) {
929 return false;
930 }
931
932 $extraquestionfields = $this->extra_question_fields();
933 if (!is_array($extraquestionfields)) {
934 return false;
935 }
936
937 //omit table name
938 array_shift($extraquestionfields);
939 $qo = $format->import_headers($data);
940 $qo->qtype = $question_type;
941
942 foreach ($extraquestionfields as $field) {
eaeb6b51 943 $qo->$field = $format->getpath($data, array('#', $field, 0, '#'), $qo->$field);
88bc20c3 944 }
945
946 // run through the answers
947 $answers = $data['#']['answer'];
948 $a_count = 0;
949 $extraasnwersfields = $this->extra_answer_fields();
950 if (is_array($extraasnwersfields)) {
eaeb6b51 951 // TODO import the answers, with any extra data.
88bc20c3 952 } else {
953 foreach ($answers as $answer) {
954 $ans = $format->import_answer($answer);
955 $qo->answer[$a_count] = $ans->answer;
956 $qo->fraction[$a_count] = $ans->fraction;
957 $qo->feedback[$a_count] = $ans->feedback;
958 ++$a_count;
959 }
960 }
961 return $qo;
962 }
963
964 /*
965 * Export question to the Moodle XML format
966 *
967 * Export question using information from extra_question_fields function
968 * If some of you fields contains id's you'll need to reimplement this
969 */
d1b7e03d 970 public function export_to_xml($question, $format, $extra=null) {
88bc20c3 971 $extraquestionfields = $this->extra_question_fields();
972 if (!is_array($extraquestionfields)) {
973 return false;
974 }
975
976 //omit table name
977 array_shift($extraquestionfields);
978 $expout='';
979 foreach ($extraquestionfields as $field) {
2d01a916
TH
980 $exportedvalue = $question->options->$field;
981 if (!empty($exportedvalue) && htmlspecialchars($exportedvalue) != $exportedvalue) {
982 $exportedvalue = '<![CDATA[' . $exportedvalue . ']]>';
983 }
984 $expout .= " <$field>{$exportedvalue}</$field>\n";
88bc20c3 985 }
986
987 $extraasnwersfields = $this->extra_answer_fields();
988 if (is_array($extraasnwersfields)) {
eaeb6b51 989 // TODO export answers with any extra data
88bc20c3 990 } else {
991 foreach ($question->options->answers as $answer) {
992 $percent = 100 * $answer->fraction;
993 $expout .= " <answer fraction=\"$percent\">\n";
994 $expout .= $format->writetext($answer->answer, 3, false);
995 $expout .= " <feedback>\n";
996 $expout .= $format->writetext($answer->feedback, 4, false);
997 $expout .= " </feedback>\n";
998 $expout .= " </answer>\n";
999 }
1000 }
1001 return $expout;
1002 }
1003
b9bd6da4 1004 /**
1005 * Abstract function implemented by each question type. It runs all the code
1006 * required to set up and save a question of any type for testing purposes.
1007 * Alternate DB table prefix may be used to facilitate data deletion.
1008 */
d1b7e03d 1009 public function generate_test($name, $courseid=null) {
b9bd6da4 1010 $form = new stdClass();
1011 $form->name = $name;
1012 $form->questiontextformat = 1;
1013 $form->questiontext = 'test question, generated by script';
d1b7e03d
TH
1014 $form->defaultmark = 1;
1015 $form->penalty = 0.3333333;
b9bd6da4 1016 $form->generalfeedback = "Well done";
1017
1018 $context = get_context_instance(CONTEXT_COURSE, $courseid);
1019 $newcategory = question_make_default_categories(array($context));
1020 $form->category = $newcategory->id . ',1';
1021
1022 $question = new stdClass();
1023 $question->courseid = $courseid;
1024 $question->qtype = $this->qtype;
1025 return array($form, $question);
1026 }
f29aeb5a
TH
1027
1028 /**
1029 * Get question context by category id
1030 * @param int $category
1031 * @return object $context
1032 */
c7df5006 1033 protected function get_context_by_category_id($category) {
f29aeb5a
TH
1034 global $DB;
1035 $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$category));
1036 $context = get_context_instance_by_id($contextid);
1037 return $context;
1038 }
1039
1040 /**
1041 * Save the file belonging to one text field.
1042 *
1043 * @param array $field the data from the form (or from import). This will
1044 * normally have come from the formslib editor element, so it will be an
1045 * array with keys 'text', 'format' and 'itemid'. However, when we are
1046 * importing, it will be an array with keys 'text', 'format' and 'files'
1047 * @param object $context the context the question is in.
1048 * @param string $component indentifies the file area question.
eaeb6b51
TH
1049 * @param string $filearea indentifies the file area questiontext,
1050 * generalfeedback, answerfeedback, etc.
f7970e3c 1051 * @param int $itemid identifies the file area.
f29aeb5a
TH
1052 *
1053 * @return string the text for this field, after files have been processed.
1054 */
1055 protected function import_or_save_files($field, $context, $component, $filearea, $itemid) {
1056 if (!empty($field['itemid'])) {
1057 // This is the normal case. We are safing the questions editing form.
1058 return file_save_draft_area_files($field['itemid'], $context->id, $component,
1059 $filearea, $itemid, $this->fileoptions, trim($field['text']));
1060
1061 } else if (!empty($field['files'])) {
1062 // This is the case when we are doing an import.
1063 foreach ($field['files'] as $file) {
1064 $this->import_file($context, $component, $filearea, $itemid, $file);
1065 }
1066 }
1067 return trim($field['text']);
1068 }
1069
1070 /**
1071 * Move all the files belonging to this question from one context to another.
f7970e3c
TH
1072 * @param int $questionid the question being moved.
1073 * @param int $oldcontextid the context it is moving from.
1074 * @param int $newcontextid the context it is moving to.
f29aeb5a
TH
1075 */
1076 public function move_files($questionid, $oldcontextid, $newcontextid) {
1077 $fs = get_file_storage();
1078 $fs->move_area_files_to_new_context($oldcontextid,
1079 $newcontextid, 'question', 'questiontext', $questionid);
1080 $fs->move_area_files_to_new_context($oldcontextid,
1081 $newcontextid, 'question', 'generalfeedback', $questionid);
1082 }
1083
1084 /**
1085 * Move all the files belonging to this question's answers when the question
1086 * is moved from one context to another.
f7970e3c
TH
1087 * @param int $questionid the question being moved.
1088 * @param int $oldcontextid the context it is moving from.
1089 * @param int $newcontextid the context it is moving to.
1090 * @param bool $answerstoo whether there is an 'answer' question area,
f29aeb5a
TH
1091 * as well as an 'answerfeedback' one. Default false.
1092 */
eaeb6b51
TH
1093 protected function move_files_in_answers($questionid, $oldcontextid,
1094 $newcontextid, $answerstoo = false) {
f29aeb5a
TH
1095 global $DB;
1096 $fs = get_file_storage();
1097
1098 $answerids = $DB->get_records_menu('question_answers',
1099 array('question' => $questionid), 'id', 'id,1');
1100 foreach ($answerids as $answerid => $notused) {
1101 if ($answerstoo) {
1102 $fs->move_area_files_to_new_context($oldcontextid,
1103 $newcontextid, 'question', 'answer', $answerid);
1104 }
1105 $fs->move_area_files_to_new_context($oldcontextid,
1106 $newcontextid, 'question', 'answerfeedback', $answerid);
1107 }
1108 }
1109
1110 /**
1111 * Delete all the files belonging to this question.
f7970e3c
TH
1112 * @param int $questionid the question being deleted.
1113 * @param int $contextid the context the question is in.
f29aeb5a
TH
1114 */
1115 protected function delete_files($questionid, $contextid) {
1116 $fs = get_file_storage();
1117 $fs->delete_area_files($contextid, 'question', 'questiontext', $questionid);
1118 $fs->delete_area_files($contextid, 'question', 'generalfeedback', $questionid);
1119 }
1120
1121 /**
1122 * Delete all the files belonging to this question's answers.
f7970e3c
TH
1123 * @param int $questionid the question being deleted.
1124 * @param int $contextid the context the question is in.
1125 * @param bool $answerstoo whether there is an 'answer' question area,
f29aeb5a
TH
1126 * as well as an 'answerfeedback' one. Default false.
1127 */
1128 protected function delete_files_in_answers($questionid, $contextid, $answerstoo = false) {
1129 global $DB;
1130 $fs = get_file_storage();
1131
1132 $answerids = $DB->get_records_menu('question_answers',
1133 array('question' => $questionid), 'id', 'id,1');
1134 foreach ($answerids as $answerid => $notused) {
1135 if ($answerstoo) {
1136 $fs->delete_area_files($contextid, 'question', 'answer', $answerid);
1137 }
1138 $fs->delete_area_files($contextid, 'question', 'answerfeedback', $answerid);
1139 }
1140 }
1141
f902a54e 1142 public function import_file($context, $component, $filearea, $itemid, $file) {
f29aeb5a 1143 $fs = get_file_storage();
0ff4bd08 1144 $record = new stdClass();
f29aeb5a
TH
1145 if (is_object($context)) {
1146 $record->contextid = $context->id;
1147 } else {
1148 $record->contextid = $context;
1149 }
1150 $record->component = $component;
1151 $record->filearea = $filearea;
1152 $record->itemid = $itemid;
1153 $record->filename = $file->name;
1154 $record->filepath = '/';
1155 return $fs->create_file_from_string($record, $this->decode_file($file));
1156 }
1157
c7df5006 1158 protected function decode_file($file) {
f29aeb5a 1159 switch ($file->encoding) {
eaeb6b51
TH
1160 case 'base64':
1161 default:
1162 return base64_decode($file->content);
f29aeb5a
TH
1163 }
1164 }
d1b7e03d 1165}
aeb15530 1166
cde2709a 1167
d1b7e03d
TH
1168/**
1169 * This class is used in the return value from
1170 * {@link question_type::get_possible_responses()}.
1171 *
7764183a
TH
1172 * @copyright 2010 The Open University
1173 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1174 */
1175class question_possible_response {
9203b705 1176 /**
d1b7e03d
TH
1177 * @var string the classification of this response the student gave to this
1178 * part of the question. Must match one of the responseclasses returned by
1179 * {@link question_type::get_possible_responses()}.
9203b705 1180 */
d1b7e03d
TH
1181 public $responseclass;
1182 /** @var string the actual response the student gave to this part. */
1183 public $fraction;
9203b705 1184 /**
d1b7e03d
TH
1185 * Constructor, just an easy way to set the fields.
1186 * @param string $responseclassid see the field descriptions above.
1187 * @param string $response see the field descriptions above.
1188 * @param number $fraction see the field descriptions above.
9203b705 1189 */
d1b7e03d
TH
1190 public function __construct($responseclass, $fraction) {
1191 $this->responseclass = $responseclass;
1192 $this->fraction = $fraction;
cde2709a
DC
1193 }
1194
d1b7e03d
TH
1195 public static function no_response() {
1196 return new question_possible_response(get_string('noresponse', 'question'), 0);
cde2709a 1197 }
fe6ce234 1198}