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