MDL-24594 qtype_multichoice should allow images in the choices.
[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
496 $oldhints = $DB->get_records('question_hints', array('questionid' => $formdata->id));
295043c2 497
d1b7e03d
TH
498 if (!empty($formdata->hint)) {
499 $numhints = max(array_keys($formdata->hint)) + 1;
500 } else {
501 $numhints = 0;
516cf3eb 502 }
295043c2 503
d1b7e03d
TH
504 if ($withparts) {
505 if (!empty($formdata->hintclearwrong)) {
506 $numclears = max(array_keys($formdata->hintclearwrong)) + 1;
507 } else {
508 $numclears = 0;
509 }
510 if (!empty($formdata->hintshownumcorrect)) {
511 $numshows = max(array_keys($formdata->hintshownumcorrect)) + 1;
512 } else {
513 $numshows = 0;
514 }
515 $numhints = max($numhints, $numclears, $numshows);
516 }
517
518 for ($i = 0; $i < $numhints; $i += 1) {
7a719748
TH
519 if (html_is_blank($formdata->hint[$i]['text'])) {
520 $formdata->hint[$i]['text'] = '';
d1b7e03d
TH
521 }
522
523 if ($withparts) {
7a719748
TH
524 $clearwrong = !empty($formdata->hintclearwrong[$i]);
525 $shownumcorrect = !empty($formdata->hintshownumcorrect[$i]);
d1b7e03d
TH
526 }
527
eaeb6b51
TH
528 if (empty($formdata->hint[$i]['text']) && empty($clearwrong) &&
529 empty($shownumcorrect)) {
d1b7e03d
TH
530 continue;
531 }
532
f3ca24e4 533 // Update an existing hint if possible.
7a719748
TH
534 $hint = array_shift($oldhints);
535 if (!$hint) {
536 $hint = new stdClass();
537 $hint->questionid = $formdata->id;
538 $hint->hint = '';
539 $hint->id = $DB->insert_record('question_hints', $hint);
540 }
541
7a719748
TH
542 $hint->hint = $this->import_or_save_files($formdata->hint[$i],
543 $context, 'question', 'hint', $hint->id);
544 $hint->hintformat = $formdata->hint[$i]['format'];
545 if ($withparts) {
546 $hint->clearwrong = $clearwrong;
547 $hint->shownumcorrect = $shownumcorrect;
548 }
549 $DB->update_record('question_hints', $hint);
550 }
551
552 // Delete any remaining old hints.
553 $fs = get_file_storage();
c73c9836 554 foreach ($oldhints as $oldhint) {
7a719748
TH
555 $fs->delete_area_files($context->id, 'question', 'hint', $oldhint->id);
556 $DB->delete_records('question_hints', array('id' => $oldhint->id));
d1b7e03d
TH
557 }
558 }
559
1c2ed7c5
TH
560 /**
561 * Can be used to {@link save_question_options()} to transfer the combined
562 * feedback fields from $formdata to $options.
563 * @param object $options the $question->options object being built.
564 * @param object $formdata the data from the form.
565 * @param object $context the context the quetsion is being saved into.
f7970e3c 566 * @param bool $withparts whether $options->shownumcorrect should be set.
1c2ed7c5 567 */
eaeb6b51
TH
568 protected function save_combined_feedback_helper($options, $formdata,
569 $context, $withparts = false) {
1c2ed7c5
TH
570 $options->correctfeedback = $this->import_or_save_files($formdata->correctfeedback,
571 $context, 'question', 'correctfeedback', $formdata->id);
572 $options->correctfeedbackformat = $formdata->correctfeedback['format'];
eaeb6b51
TH
573
574 $options->partiallycorrectfeedback = $this->import_or_save_files(
575 $formdata->partiallycorrectfeedback,
1c2ed7c5
TH
576 $context, 'question', 'partiallycorrectfeedback', $formdata->id);
577 $options->partiallycorrectfeedbackformat = $formdata->partiallycorrectfeedback['format'];
eaeb6b51 578
1c2ed7c5
TH
579 $options->incorrectfeedback = $this->import_or_save_files($formdata->incorrectfeedback,
580 $context, 'question', 'incorrectfeedback', $formdata->id);
581 $options->incorrectfeedbackformat = $formdata->incorrectfeedback['format'];
582
583 if ($withparts) {
584 $options->shownumcorrect = !empty($formdata->shownumcorrect);
585 }
586
587 return $options;
588 }
589
d1b7e03d
TH
590 /**
591 * Loads the question type specific options for the question.
592 *
593 * This function loads any question type specific options for the
594 * question from the database into the question object. This information
595 * is placed in the $question->options field. A question type is
596 * free, however, to decide on a internal structure of the options field.
597 * @return bool Indicates success or failure.
598 * @param object $question The question object for the question. This object
599 * should be updated to include the question type
600 * specific information (it is passed by reference).
601 */
602 public function get_question_options($question) {
f29aeb5a
TH
603 global $CFG, $DB, $OUTPUT;
604
605 if (!isset($question->options)) {
606 $question->options = new stdClass();
607 }
d1b7e03d 608
eaeb6b51
TH
609 $extraquestionfields = $this->extra_question_fields();
610 if (is_array($extraquestionfields)) {
611 $question_extension_table = array_shift($extraquestionfields);
612 $extra_data = $DB->get_record($question_extension_table,
613 array($this->questionid_column_name() => $question->id),
614 implode(', ', $extraquestionfields));
295043c2 615 if ($extra_data) {
eaeb6b51 616 foreach ($extraquestionfields as $field) {
295043c2 617 $question->options->$field = $extra_data->$field;
618 }
619 } else {
eaeb6b51
TH
620 echo $OUTPUT->notification('Failed to load question options from the table ' .
621 $question_extension_table . ' for questionid ' . $question->id);
295043c2 622 return false;
623 }
624 }
625
eaeb6b51
TH
626 $extraanswerfields = $this->extra_answer_fields();
627 if (is_array($extraanswerfields)) {
628 $answer_extension_table = array_shift($extraanswerfields);
f29aeb5a 629 $question->options->answers = $DB->get_records_sql("
eaeb6b51 630 SELECT qa.*, qax." . implode(', qax.', $extraanswerfields) . "
f29aeb5a
TH
631 FROM {question_answers} qa, {$answer_extension_table} qax
632 WHERE qa.questionid = ? AND qax.answerid = qa.id", array($question->id));
295043c2 633 if (!$question->options->answers) {
eaeb6b51
TH
634 echo $OUTPUT->notification('Failed to load question answers from the table ' .
635 $answer_extension_table . 'for questionid ' . $question->id);
295043c2 636 return false;
637 }
638 } else {
eaeb6b51
TH
639 // Don't check for success or failure because some question types do
640 // not use the answers table.
641 $question->options->answers = $DB->get_records('question_answers',
642 array('question' => $question->id), 'id ASC');
295043c2 643 }
644
eaeb6b51
TH
645 $question->hints = $DB->get_records('question_hints',
646 array('questionid' => $question->id), 'id ASC');
d1b7e03d 647
516cf3eb 648 return true;
649 }
650
651 /**
d1b7e03d
TH
652 * Create an appropriate question_definition for the question of this type
653 * using data loaded from the database.
654 * @param object $questiondata the question data loaded from the database.
655 * @return question_definition the corresponding question_definition.
656 */
657 public function make_question($questiondata) {
658 $question = $this->make_question_instance($questiondata);
659 $this->initialise_question_instance($question, $questiondata);
660 return $question;
661 }
0429cd86 662
d1b7e03d
TH
663 /**
664 * Create an appropriate question_definition for the question of this type
665 * using data loaded from the database.
666 * @param object $questiondata the question data loaded from the database.
667 * @return question_definition an instance of the appropriate question_definition subclass.
668 * Still needs to be initialised.
669 */
670 protected function make_question_instance($questiondata) {
671 question_bank::load_question_definition_classes($this->name());
672 $class = 'qtype_' . $this->name() . '_question';
673 return new $class();
674 }
675
676 /**
677 * Initialise the common question_definition fields.
678 * @param question_definition $question the question_definition we are creating.
679 * @param object $questiondata the question data loaded from the database.
680 */
681 protected function initialise_question_instance(question_definition $question, $questiondata) {
682 $question->id = $questiondata->id;
683 $question->category = $questiondata->category;
56e82d99 684 $question->contextid = $questiondata->contextid;
d1b7e03d
TH
685 $question->parent = $questiondata->parent;
686 $question->qtype = $this;
687 $question->name = $questiondata->name;
688 $question->questiontext = $questiondata->questiontext;
689 $question->questiontextformat = $questiondata->questiontextformat;
690 $question->generalfeedback = $questiondata->generalfeedback;
1c2ed7c5 691 $question->generalfeedbackformat = $questiondata->generalfeedbackformat;
d1b7e03d
TH
692 $question->defaultmark = $questiondata->defaultmark + 0;
693 $question->length = $questiondata->length;
694 $question->penalty = $questiondata->penalty;
695 $question->stamp = $questiondata->stamp;
696 $question->version = $questiondata->version;
697 $question->hidden = $questiondata->hidden;
698 $question->timecreated = $questiondata->timecreated;
699 $question->timemodified = $questiondata->timemodified;
700 $question->createdby = $questiondata->createdby;
701 $question->modifiedby = $questiondata->modifiedby;
702
703 $this->initialise_question_hints($question, $questiondata);
704 }
705
706 /**
707 * Initialise question_definition::hints field.
708 * @param question_definition $question the question_definition we are creating.
709 * @param object $questiondata the question data loaded from the database.
710 */
711 protected function initialise_question_hints(question_definition $question, $questiondata) {
712 if (empty($questiondata->hints)) {
713 return;
714 }
715 foreach ($questiondata->hints as $hint) {
716 $question->hints[] = $this->make_hint($hint);
717 }
0429cd86 718 }
719
720 /**
d1b7e03d
TH
721 * Create a question_hint, or an appropriate subclass for this question,
722 * from a row loaded from the database.
723 * @param object $hint the DB row from the question hints table.
724 * @return question_hint
9203b705 725 */
d1b7e03d
TH
726 protected function make_hint($hint) {
727 return question_hint::load_from_record($hint);
728 }
729
1c2ed7c5
TH
730 /**
731 * Initialise the combined feedback fields.
732 * @param question_definition $question the question_definition we are creating.
733 * @param object $questiondata the question data loaded from the database.
f7970e3c 734 * @param bool $withparts whether to set the shownumcorrect field.
1c2ed7c5 735 */
eaeb6b51
TH
736 protected function initialise_combined_feedback(question_definition $question,
737 $questiondata, $withparts = false) {
1c2ed7c5
TH
738 $question->correctfeedback = $questiondata->options->correctfeedback;
739 $question->correctfeedbackformat = $questiondata->options->correctfeedbackformat;
740 $question->partiallycorrectfeedback = $questiondata->options->partiallycorrectfeedback;
eaeb6b51
TH
741 $question->partiallycorrectfeedbackformat =
742 $questiondata->options->partiallycorrectfeedbackformat;
1c2ed7c5
TH
743 $question->incorrectfeedback = $questiondata->options->incorrectfeedback;
744 $question->incorrectfeedbackformat = $questiondata->options->incorrectfeedbackformat;
745 if ($withparts) {
746 $question->shownumcorrect = $questiondata->options->shownumcorrect;
747 }
748 }
749
d1b7e03d
TH
750 /**
751 * Initialise question_definition::answers field.
752 * @param question_definition $question the question_definition we are creating.
753 * @param object $questiondata the question data loaded from the database.
754 */
755 protected function initialise_question_answers(question_definition $question, $questiondata) {
756 $question->answers = array();
757 if (empty($questiondata->options->answers)) {
758 return;
759 }
760 foreach ($questiondata->options->answers as $a) {
5f7cfba7
TH
761 $question->answers[$a->id] = new question_answer($a->id, $a->answer,
762 $a->fraction, $a->feedback, $a->feedbackformat);
d1b7e03d
TH
763 }
764 }
9203b705 765
d1b7e03d 766 /**
f29aeb5a 767 * Deletes the question-type specific data when a question is deleted.
f7970e3c
TH
768 * @param int $question the question being deleted.
769 * @param int $contextid the context this quesiotn belongs to.
f29aeb5a
TH
770 */
771 public function delete_question($questionid, $contextid) {
772 global $DB;
773
774 $this->delete_files($questionid, $contextid);
516cf3eb 775
eaeb6b51
TH
776 $extraquestionfields = $this->extra_question_fields();
777 if (is_array($extraquestionfields)) {
778 $question_extension_table = array_shift($extraquestionfields);
f29aeb5a
TH
779 $DB->delete_records($question_extension_table,
780 array($this->questionid_column_name() => $questionid));
295043c2 781 }
782
eaeb6b51
TH
783 $extraanswerfields = $this->extra_answer_fields();
784 if (is_array($extraanswerfields)) {
785 $answer_extension_table = array_shift($extraanswerfields);
f29aeb5a 786 $DB->delete_records_select($answer_extension_table,
eaeb6b51
TH
787 'answerid IN (SELECT qa.id FROM {question_answers} qa WHERE qa.question = ?)',
788 array($questionid));
295043c2 789 }
790
f29aeb5a 791 $DB->delete_records('question_answers', array('question' => $questionid));
d1b7e03d 792
f29aeb5a 793 $DB->delete_records('question_hints', array('questionid' => $questionid));
516cf3eb 794 }
795
796 /**
eaeb6b51
TH
797 * Returns the number of question numbers which are used by the question
798 *
799 * This function returns the number of question numbers to be assigned
800 * to the question. Most question types will have length one; they will be
801 * assigned one number. The 'description' type, however does not use up a
802 * number and so has a length of zero. Other question types may wish to
803 * handle a bundle of questions and hence return a number greater than one.
804 * @return int The number of question numbers which should be
805 * assigned to the question.
806 * @param object $question The question whose length is to be determined.
807 * Question type specific information is included.
808 */
d1b7e03d 809 public function actual_number_of_questions($question) {
516cf3eb 810 // By default, each question is given one number
811 return 1;
812 }
813
6f51ed72 814 /**
815 * @param object $question
d1b7e03d
TH
816 * @return number|null either a fraction estimating what the student would
817 * score by guessing, or null, if it is not possible to estimate.
6f51ed72 818 */
c7df5006 819 public function get_random_guess_score($questiondata) {
6f51ed72 820 return 0;
821 }
516cf3eb 822
516cf3eb 823 /**
d1b7e03d 824 * This method should return all the possible types of response that are
f9b0500f 825 * recognised for this question.
271e6dec 826 *
d1b7e03d
TH
827 * The question is modelled as comprising one or more subparts. For each
828 * subpart, there are one or more classes that that students response
829 * might fall into, each of those classes earning a certain score.
50da63eb 830 *
d1b7e03d
TH
831 * For example, in a shortanswer question, there is only one subpart, the
832 * text entry field. The response the student gave will be classified according
833 * to which of the possible $question->options->answers it matches.
271e6dec 834 *
d1b7e03d
TH
835 * For the matching question type, there will be one subpart for each
836 * question stem, and for each stem, each of the possible choices is a class
837 * of student's response.
838 *
839 * A response is an object with two fields, ->responseclass is a string
840 * presentation of that response, and ->fraction, the credit for a response
841 * in that class.
842 *
843 * Array keys have no specific meaning, but must be unique, and must be
844 * the same if this function is called repeatedly.
845 *
846 * @param object $question the question definition data.
847 * @return array keys are subquestionid, values are arrays of possible
848 * responses to that subquestion.
99ba746d 849 */
c7df5006 850 public function get_possible_responses($questiondata) {
d1b7e03d 851 return array();
50da63eb 852 }
5fceb049 853
50da63eb 854 /**
f29aeb5a
TH
855 * Like @see{get_html_head_contributions}, but this method is for CSS and
856 * JavaScript required on the question editing page question/question.php.
50da63eb 857 */
d1b7e03d 858 public function get_editing_head_contributions() {
50da63eb 859 // By default, we link to any of the files styles.css, styles.php,
860 // script.js or script.php that exist in the plugin folder.
861 // Core question types should not use this mechanism. Their styles
862 // should be included in the standard theme.
f29aeb5a 863 $this->find_standard_scripts();
50da63eb 864 }
271e6dec 865
50da63eb 866 /**
f29aeb5a 867 * Utility method used by @see{get_html_head_contributions} and
50da63eb 868 * @see{get_editing_head_contributions}. This looks for any of the files
f29aeb5a
TH
869 * script.js or script.php that exist in the plugin folder and ensures they
870 * get included.
50da63eb 871 */
c76145d3 872 public function find_standard_scripts() {
f29aeb5a
TH
873 global $PAGE;
874
99ba746d 875 $plugindir = $this->plugin_dir();
f29aeb5a 876 $plugindirrel = 'question/type/' . $this->name();
50da63eb 877
878 if (file_exists($plugindir . '/script.js')) {
f29aeb5a 879 $PAGE->requires->js('/' . $plugindirrel . '/script.js');
50da63eb 880 }
881 if (file_exists($plugindir . '/script.php')) {
f29aeb5a 882 $PAGE->requires->js('/' . $plugindirrel . '/script.php');
516cf3eb 883 }
fe9b5cfd 884 }
885
62e76c67 886 /**
eaeb6b51
TH
887 * Returns true if the editing wizard is finished, false otherwise.
888 *
889 * The default implementation returns true, which is suitable for all question-
890 * types that only use one editing form. This function is used in
891 * question.php to decide whether we can regrade any states of the edited
892 * question and redirect to edit.php.
893 *
894 * The dataset dependent question-type, which is extended by the calculated
895 * question-type, overwrites this method because it uses multiple pages (i.e.
896 * a wizard) to set up the question and associated datasets.
897 *
898 * @param object $form The data submitted by the previous page.
899 *
900 * @return bool Whether the wizard's last page was submitted or not.
901 */
f29aeb5a 902 public function finished_edit_wizard($form) {
d1b7e03d
TH
903 //In the default case there is only one edit page.
904 return true;
62e76c67 905 }
906
eaeb6b51 907 /// IMPORT/EXPORT FUNCTIONS /////////////////
88bc20c3 908
909 /*
910 * Imports question from the Moodle XML format
911 *
912 * Imports question using information from extra_question_fields function
913 * If some of you fields contains id's you'll need to reimplement this
914 */
d1b7e03d 915 public function import_from_xml($data, $question, $format, $extra=null) {
88bc20c3 916 $question_type = $data['@']['type'];
917 if ($question_type != $this->name()) {
918 return false;
919 }
920
921 $extraquestionfields = $this->extra_question_fields();
922 if (!is_array($extraquestionfields)) {
923 return false;
924 }
925
926 //omit table name
927 array_shift($extraquestionfields);
928 $qo = $format->import_headers($data);
929 $qo->qtype = $question_type;
930
931 foreach ($extraquestionfields as $field) {
eaeb6b51 932 $qo->$field = $format->getpath($data, array('#', $field, 0, '#'), $qo->$field);
88bc20c3 933 }
934
935 // run through the answers
936 $answers = $data['#']['answer'];
937 $a_count = 0;
938 $extraasnwersfields = $this->extra_answer_fields();
939 if (is_array($extraasnwersfields)) {
eaeb6b51 940 // TODO import the answers, with any extra data.
88bc20c3 941 } else {
942 foreach ($answers as $answer) {
943 $ans = $format->import_answer($answer);
944 $qo->answer[$a_count] = $ans->answer;
945 $qo->fraction[$a_count] = $ans->fraction;
946 $qo->feedback[$a_count] = $ans->feedback;
947 ++$a_count;
948 }
949 }
950 return $qo;
951 }
952
953 /*
954 * Export question to the Moodle XML format
955 *
956 * Export question using information from extra_question_fields function
957 * If some of you fields contains id's you'll need to reimplement this
958 */
d1b7e03d 959 public function export_to_xml($question, $format, $extra=null) {
88bc20c3 960 $extraquestionfields = $this->extra_question_fields();
961 if (!is_array($extraquestionfields)) {
962 return false;
963 }
964
965 //omit table name
966 array_shift($extraquestionfields);
967 $expout='';
968 foreach ($extraquestionfields as $field) {
2d01a916
TH
969 $exportedvalue = $question->options->$field;
970 if (!empty($exportedvalue) && htmlspecialchars($exportedvalue) != $exportedvalue) {
971 $exportedvalue = '<![CDATA[' . $exportedvalue . ']]>';
972 }
973 $expout .= " <$field>{$exportedvalue}</$field>\n";
88bc20c3 974 }
975
976 $extraasnwersfields = $this->extra_answer_fields();
977 if (is_array($extraasnwersfields)) {
eaeb6b51 978 // TODO export answers with any extra data
88bc20c3 979 } else {
980 foreach ($question->options->answers as $answer) {
981 $percent = 100 * $answer->fraction;
982 $expout .= " <answer fraction=\"$percent\">\n";
983 $expout .= $format->writetext($answer->answer, 3, false);
984 $expout .= " <feedback>\n";
985 $expout .= $format->writetext($answer->feedback, 4, false);
986 $expout .= " </feedback>\n";
987 $expout .= " </answer>\n";
988 }
989 }
990 return $expout;
991 }
992
b9bd6da4 993 /**
994 * Abstract function implemented by each question type. It runs all the code
995 * required to set up and save a question of any type for testing purposes.
996 * Alternate DB table prefix may be used to facilitate data deletion.
997 */
d1b7e03d 998 public function generate_test($name, $courseid=null) {
b9bd6da4 999 $form = new stdClass();
1000 $form->name = $name;
1001 $form->questiontextformat = 1;
1002 $form->questiontext = 'test question, generated by script';
d1b7e03d
TH
1003 $form->defaultmark = 1;
1004 $form->penalty = 0.3333333;
b9bd6da4 1005 $form->generalfeedback = "Well done";
1006
1007 $context = get_context_instance(CONTEXT_COURSE, $courseid);
1008 $newcategory = question_make_default_categories(array($context));
1009 $form->category = $newcategory->id . ',1';
1010
1011 $question = new stdClass();
1012 $question->courseid = $courseid;
1013 $question->qtype = $this->qtype;
1014 return array($form, $question);
1015 }
f29aeb5a
TH
1016
1017 /**
1018 * Get question context by category id
1019 * @param int $category
1020 * @return object $context
1021 */
c7df5006 1022 protected function get_context_by_category_id($category) {
f29aeb5a
TH
1023 global $DB;
1024 $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$category));
1025 $context = get_context_instance_by_id($contextid);
1026 return $context;
1027 }
1028
1029 /**
1030 * Save the file belonging to one text field.
1031 *
1032 * @param array $field the data from the form (or from import). This will
1033 * normally have come from the formslib editor element, so it will be an
1034 * array with keys 'text', 'format' and 'itemid'. However, when we are
1035 * importing, it will be an array with keys 'text', 'format' and 'files'
1036 * @param object $context the context the question is in.
1037 * @param string $component indentifies the file area question.
eaeb6b51
TH
1038 * @param string $filearea indentifies the file area questiontext,
1039 * generalfeedback, answerfeedback, etc.
f7970e3c 1040 * @param int $itemid identifies the file area.
f29aeb5a
TH
1041 *
1042 * @return string the text for this field, after files have been processed.
1043 */
1044 protected function import_or_save_files($field, $context, $component, $filearea, $itemid) {
1045 if (!empty($field['itemid'])) {
1046 // This is the normal case. We are safing the questions editing form.
1047 return file_save_draft_area_files($field['itemid'], $context->id, $component,
1048 $filearea, $itemid, $this->fileoptions, trim($field['text']));
1049
1050 } else if (!empty($field['files'])) {
1051 // This is the case when we are doing an import.
1052 foreach ($field['files'] as $file) {
1053 $this->import_file($context, $component, $filearea, $itemid, $file);
1054 }
1055 }
1056 return trim($field['text']);
1057 }
1058
1059 /**
1060 * Move all the files belonging to this question from one context to another.
f7970e3c
TH
1061 * @param int $questionid the question being moved.
1062 * @param int $oldcontextid the context it is moving from.
1063 * @param int $newcontextid the context it is moving to.
f29aeb5a
TH
1064 */
1065 public function move_files($questionid, $oldcontextid, $newcontextid) {
1066 $fs = get_file_storage();
1067 $fs->move_area_files_to_new_context($oldcontextid,
1068 $newcontextid, 'question', 'questiontext', $questionid);
1069 $fs->move_area_files_to_new_context($oldcontextid,
1070 $newcontextid, 'question', 'generalfeedback', $questionid);
1071 }
1072
1073 /**
1074 * Move all the files belonging to this question's answers when the question
1075 * is moved from one context to another.
f7970e3c
TH
1076 * @param int $questionid the question being moved.
1077 * @param int $oldcontextid the context it is moving from.
1078 * @param int $newcontextid the context it is moving to.
1079 * @param bool $answerstoo whether there is an 'answer' question area,
f29aeb5a
TH
1080 * as well as an 'answerfeedback' one. Default false.
1081 */
eaeb6b51
TH
1082 protected function move_files_in_answers($questionid, $oldcontextid,
1083 $newcontextid, $answerstoo = false) {
f29aeb5a
TH
1084 global $DB;
1085 $fs = get_file_storage();
1086
1087 $answerids = $DB->get_records_menu('question_answers',
1088 array('question' => $questionid), 'id', 'id,1');
1089 foreach ($answerids as $answerid => $notused) {
1090 if ($answerstoo) {
1091 $fs->move_area_files_to_new_context($oldcontextid,
1092 $newcontextid, 'question', 'answer', $answerid);
1093 }
1094 $fs->move_area_files_to_new_context($oldcontextid,
1095 $newcontextid, 'question', 'answerfeedback', $answerid);
1096 }
1097 }
1098
1099 /**
1100 * Delete all the files belonging to this question.
f7970e3c
TH
1101 * @param int $questionid the question being deleted.
1102 * @param int $contextid the context the question is in.
f29aeb5a
TH
1103 */
1104 protected function delete_files($questionid, $contextid) {
1105 $fs = get_file_storage();
1106 $fs->delete_area_files($contextid, 'question', 'questiontext', $questionid);
1107 $fs->delete_area_files($contextid, 'question', 'generalfeedback', $questionid);
1108 }
1109
1110 /**
1111 * Delete all the files belonging to this question's answers.
f7970e3c
TH
1112 * @param int $questionid the question being deleted.
1113 * @param int $contextid the context the question is in.
1114 * @param bool $answerstoo whether there is an 'answer' question area,
f29aeb5a
TH
1115 * as well as an 'answerfeedback' one. Default false.
1116 */
1117 protected function delete_files_in_answers($questionid, $contextid, $answerstoo = false) {
1118 global $DB;
1119 $fs = get_file_storage();
1120
1121 $answerids = $DB->get_records_menu('question_answers',
1122 array('question' => $questionid), 'id', 'id,1');
1123 foreach ($answerids as $answerid => $notused) {
1124 if ($answerstoo) {
1125 $fs->delete_area_files($contextid, 'question', 'answer', $answerid);
1126 }
1127 $fs->delete_area_files($contextid, 'question', 'answerfeedback', $answerid);
1128 }
1129 }
1130
f902a54e 1131 public function import_file($context, $component, $filearea, $itemid, $file) {
f29aeb5a 1132 $fs = get_file_storage();
0ff4bd08 1133 $record = new stdClass();
f29aeb5a
TH
1134 if (is_object($context)) {
1135 $record->contextid = $context->id;
1136 } else {
1137 $record->contextid = $context;
1138 }
1139 $record->component = $component;
1140 $record->filearea = $filearea;
1141 $record->itemid = $itemid;
1142 $record->filename = $file->name;
1143 $record->filepath = '/';
1144 return $fs->create_file_from_string($record, $this->decode_file($file));
1145 }
1146
c7df5006 1147 protected function decode_file($file) {
f29aeb5a 1148 switch ($file->encoding) {
eaeb6b51
TH
1149 case 'base64':
1150 default:
1151 return base64_decode($file->content);
f29aeb5a
TH
1152 }
1153 }
d1b7e03d 1154}
aeb15530 1155
cde2709a 1156
d1b7e03d
TH
1157/**
1158 * This class is used in the return value from
1159 * {@link question_type::get_possible_responses()}.
1160 *
7764183a
TH
1161 * @copyright 2010 The Open University
1162 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1163 */
1164class question_possible_response {
9203b705 1165 /**
d1b7e03d
TH
1166 * @var string the classification of this response the student gave to this
1167 * part of the question. Must match one of the responseclasses returned by
1168 * {@link question_type::get_possible_responses()}.
9203b705 1169 */
d1b7e03d
TH
1170 public $responseclass;
1171 /** @var string the actual response the student gave to this part. */
1172 public $fraction;
9203b705 1173 /**
d1b7e03d
TH
1174 * Constructor, just an easy way to set the fields.
1175 * @param string $responseclassid see the field descriptions above.
1176 * @param string $response see the field descriptions above.
1177 * @param number $fraction see the field descriptions above.
9203b705 1178 */
d1b7e03d
TH
1179 public function __construct($responseclass, $fraction) {
1180 $this->responseclass = $responseclass;
1181 $this->fraction = $fraction;
cde2709a
DC
1182 }
1183
d1b7e03d
TH
1184 public static function no_response() {
1185 return new question_possible_response(get_string('noresponse', 'question'), 0);
cde2709a 1186 }
fe6ce234 1187}