MDL-28268 Missing ORDER BY when using extra answer table.
[moodle.git] / question / type / questiontypebase.php
blob75766690ca62d47cc537eca43e73b6f4809eaf42
1 <?php
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.
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/>.
17 /**
18 * The default questiontype class.
20 * @package moodlecore
21 * @subpackage questiontypes
22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/question/engine/lib.php');
32 /**
33 * This is the base class for Moodle question types.
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.
38 * Note: the questiontype API should NOT be considered stable yet. Very few
39 * question types have been produced yet, so we do not yet know all the places
40 * where the current API is insufficient. I would rather learn from the
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.
45 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
48 class question_type {
49 protected $fileoptions = array(
50 'subdirs' => false,
51 'maxfiles' => -1,
52 'maxbytes' => 0,
55 public function __construct() {
58 /**
59 * @return string the name of this question type.
61 public function name() {
62 return substr(get_class($this), 6);
65 /**
66 * @return string the full frankenstyle name for this plugin.
68 public function plugin_name() {
69 return get_class($this);
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.
76 public function local_name() {
77 return get_string($this->name(), $this->plugin_name());
80 /**
81 * The name this question should appear as in the create new question
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.
86 * @return mixed the desired string, or false to hide this question type in the menu.
88 public function menu_name() {
89 return $this->local_name();
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.
99 * @return array any other question types that this one relies on. An empty
100 * array if none.
102 public function requires_qtypes() {
103 return array();
107 * @return bool override this to return false if this is not really a
108 * question type, for example the description question type is not
109 * really a question type.
111 public function is_real_question_type() {
112 return true;
116 * @return bool true if this question type sometimes requires manual grading.
118 public function is_manual_graded() {
119 return false;
123 * @param object $question a question of this type.
124 * @param string $otherquestionsinuse comma-separate list of other question ids in this attempt.
125 * @return bool true if a particular instance of this question requires manual grading.
127 public function is_question_manual_graded($question, $otherquestionsinuse) {
128 return $this->is_manual_graded();
132 * @return bool true if this question type can be used by the random question type.
134 public function is_usable_by_random() {
135 return true;
139 * Whether this question type can perform a frequency analysis of student
140 * responses.
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.
146 * @return bool whether this report can analyse all the student reponses
147 * for things like the quiz statistics report.
149 public function can_analyse_responses() {
150 // This works in most cases.
151 return !$this->is_manual_graded();
155 * @return whether the question_answers.answer field needs to have
156 * restore_decode_content_links_worker called on it.
158 public function has_html_answers() {
159 return false;
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).
168 * @return mixed array as above, or null to tell the base class to do nothing.
170 public function extra_question_fields() {
171 return null;
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
178 protected function questionid_column_name() {
179 return 'questionid';
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).
187 * @return mixed array as above, or null to tell the base class to do nothing.
189 protected function extra_answer_fields() {
190 return null;
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').
198 * @return array response variable names that may have associated files.
200 public function response_file_areas() {
201 return array();
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
207 * question/type/{$this->name()}/edit_{$this->name()}_question_form.php
208 * and if it exists returns an instance of it.
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.
213 public function create_editing_form($submiturl, $question, $category,
214 $contexts, $formeditable) {
215 global $CFG;
216 require_once($CFG->dirroot . '/question/type/edit_question_form.php');
217 $definitionfile = $CFG->dirroot . '/question/type/' . $this->name() .
218 '/edit_' . $this->name() . '_form.php';
219 if (!is_readable($definitionfile) || !is_file($definitionfile)) {
220 throw new coding_exception($this->plugin_name() .
221 ' is missing the definition of its editing formin file ' .
222 $definitionfile . '.');
224 require_once($definitionfile);
225 $classname = $this->plugin_name() . '_edit_form';
226 if (!class_exists($classname)) {
227 throw new coding_exception($this->plugin_name() .
228 ' does not define the class ' . $this->plugin_name() .
229 '_edit_form.');
231 return new $classname($submiturl, $question, $category, $contexts, $formeditable);
235 * @return string the full path of the folder this plugin's files live in.
237 public function plugin_dir() {
238 global $CFG;
239 return $CFG->dirroot . '/question/type/' . $this->name();
243 * @return string the URL of the folder this plugin's files live in.
245 public function plugin_baseurl() {
246 global $CFG;
247 return $CFG->wwwroot . '/question/type/' . $this->name();
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.
254 * @param question_edit_form $mform a child of question_edit_form
255 * @param object $question
256 * @param string $wizardnow is '' for first page.
258 public function display_question_editing_page($mform, $question, $wizardnow) {
259 global $OUTPUT;
260 $heading = $this->get_heading(empty($question->id));
262 echo $OUTPUT->heading_with_help($heading, $this->name(), $this->plugin_name());
264 $permissionstrs = array();
265 if (!empty($question->id)) {
266 if ($question->formoptions->canedit) {
267 $permissionstrs[] = get_string('permissionedit', 'question');
269 if ($question->formoptions->canmove) {
270 $permissionstrs[] = get_string('permissionmove', 'question');
272 if ($question->formoptions->cansaveasnew) {
273 $permissionstrs[] = get_string('permissionsaveasnew', 'question');
276 if (!$question->formoptions->movecontext && count($permissionstrs)) {
277 echo $OUTPUT->heading(get_string('permissionto', 'question'), 3);
278 $html = '<ul>';
279 foreach ($permissionstrs as $permissionstr) {
280 $html .= '<li>'.$permissionstr.'</li>';
282 $html .= '</ul>';
283 echo $OUTPUT->box($html, 'boxwidthnarrow boxaligncenter generalbox');
285 $mform->display();
289 * Method called by display_question_editing_page and by question.php to get
290 * heading for breadcrumbs.
292 * @return string the heading
294 public function get_heading($adding = false) {
295 if ($adding) {
296 $action = 'adding';
297 } else {
298 $action = 'editing';
300 return get_string($action . $this->name(), $this->plugin_name());
304 * Set any missing settings for this question to the default values. This is
305 * called before displaying the question editing form.
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.
311 public function set_default_options($questiondata) {
315 * Saves (creates or updates) a question.
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.
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.
327 * The question will be saved in category $form->category.
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.)
341 public function save_question($question, $form) {
342 global $USER, $DB, $OUTPUT;
344 list($question->category) = explode(',', $form->category);
345 $context = $this->get_context_by_category_id($question->category);
347 // This default implementation is suitable for most
348 // question types.
350 // First, save the basic question itself
351 $question->name = trim($form->name);
352 $question->parent = isset($form->parent) ? $form->parent : 0;
353 $question->length = $this->actual_number_of_questions($question);
354 $question->penalty = isset($form->penalty) ? $form->penalty : 0;
356 if (empty($form->questiontext['text'])) {
357 $question->questiontext = '';
358 } else {
359 $question->questiontext = trim($form->questiontext['text']);;
361 $question->questiontextformat = !empty($form->questiontext['format']) ?
362 $form->questiontext['format'] : 0;
364 if (empty($form->generalfeedback['text'])) {
365 $question->generalfeedback = '';
366 } else {
367 $question->generalfeedback = trim($form->generalfeedback['text']);
369 $question->generalfeedbackformat = !empty($form->generalfeedback['format']) ?
370 $form->generalfeedback['format'] : 0;
372 if (empty($question->name)) {
373 $question->name = shorten_text(strip_tags($form->questiontext['text']), 15);
374 if (empty($question->name)) {
375 $question->name = '-';
379 if ($question->penalty > 1 or $question->penalty < 0) {
380 $question->errors['penalty'] = get_string('invalidpenalty', 'question');
383 if (isset($form->defaultmark)) {
384 $question->defaultmark = $form->defaultmark;
387 // If the question is new, create it.
388 if (empty($question->id)) {
389 // Set the unique code
390 $question->stamp = make_unique_id_code();
391 $question->createdby = $USER->id;
392 $question->timecreated = time();
393 $question->id = $DB->insert_record('question', $question);
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();
402 if (!empty($question->questiontext) && !empty($form->questiontext['itemid'])) {
403 $question->questiontext = file_save_draft_area_files($form->questiontext['itemid'],
404 $context->id, 'question', 'questiontext', (int)$question->id,
405 $this->fileoptions, $question->questiontext);
407 if (!empty($question->generalfeedback) && !empty($form->generalfeedback['itemid'])) {
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);
413 $DB->update_record('question', $question);
415 // Now to save all the answers and type-specific options
416 $form->id = $question->id;
417 $form->qtype = $question->qtype;
418 $form->category = $question->category;
419 $form->questiontext = $question->questiontext;
420 $form->questiontextformat = $question->questiontextformat;
421 // current context
422 $form->context = $context;
424 $result = $this->save_question_options($form);
426 if (!empty($result->error)) {
427 print_error($result->error);
430 if (!empty($result->notice)) {
431 notice($result->notice, "question.php?id=$question->id");
434 if (!empty($result->noticeyesno)) {
435 throw new coding_exception(
436 '$result->noticeyesno no longer supported in save_question.');
439 // Give the question a unique version stamp determined by question_hash()
440 $DB->set_field('question', 'version', question_hash($question),
441 array('id' => $question->id));
443 return $question;
447 * Saves question-type specific options
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.
454 public function save_question_options($question) {
455 global $DB;
456 $extraquestionfields = $this->extra_question_fields();
458 if (is_array($extraquestionfields)) {
459 $question_extension_table = array_shift($extraquestionfields);
461 $function = 'update_record';
462 $questionidcolname = $this->questionid_column_name();
463 $options = $DB->get_record($question_extension_table,
464 array($questionidcolname => $question->id));
465 if (!$options) {
466 $function = 'insert_record';
467 $options = new stdClass();
468 $options->$questionidcolname = $question->id;
470 foreach ($extraquestionfields as $field) {
471 if (!isset($question->$field)) {
472 $result = new stdClass();
473 $result->error = "No data for field $field when saving " .
474 $this->name() . ' question id ' . $question->id;
475 return $result;
477 $options->$field = $question->$field;
480 if (!$DB->{$function}($question_extension_table, $options)) {
481 $result = new stdClass();
482 $result->error = 'Could not save question options for ' .
483 $this->name() . ' question id ' . $question->id;
484 return $result;
488 $extraanswerfields = $this->extra_answer_fields();
489 // TODO save the answers, with any extra data.
492 public function save_hints($formdata, $withparts = false) {
493 global $DB;
494 $context = $formdata->context;
496 $oldhints = $DB->get_records('question_hints',
497 array('questionid' => $formdata->id), 'id ASC');
499 if (!empty($formdata->hint)) {
500 $numhints = max(array_keys($formdata->hint)) + 1;
501 } else {
502 $numhints = 0;
505 if ($withparts) {
506 if (!empty($formdata->hintclearwrong)) {
507 $numclears = max(array_keys($formdata->hintclearwrong)) + 1;
508 } else {
509 $numclears = 0;
511 if (!empty($formdata->hintshownumcorrect)) {
512 $numshows = max(array_keys($formdata->hintshownumcorrect)) + 1;
513 } else {
514 $numshows = 0;
516 $numhints = max($numhints, $numclears, $numshows);
519 for ($i = 0; $i < $numhints; $i += 1) {
520 if (html_is_blank($formdata->hint[$i]['text'])) {
521 $formdata->hint[$i]['text'] = '';
524 if ($withparts) {
525 $clearwrong = !empty($formdata->hintclearwrong[$i]);
526 $shownumcorrect = !empty($formdata->hintshownumcorrect[$i]);
529 if (empty($formdata->hint[$i]['text']) && empty($clearwrong) &&
530 empty($shownumcorrect)) {
531 continue;
534 // Update an existing hint if possible.
535 $hint = array_shift($oldhints);
536 if (!$hint) {
537 $hint = new stdClass();
538 $hint->questionid = $formdata->id;
539 $hint->hint = '';
540 $hint->id = $DB->insert_record('question_hints', $hint);
543 $hint->hint = $this->import_or_save_files($formdata->hint[$i],
544 $context, 'question', 'hint', $hint->id);
545 $hint->hintformat = $formdata->hint[$i]['format'];
546 if ($withparts) {
547 $hint->clearwrong = $clearwrong;
548 $hint->shownumcorrect = $shownumcorrect;
550 $DB->update_record('question_hints', $hint);
553 // Delete any remaining old hints.
554 $fs = get_file_storage();
555 foreach ($oldhints as $oldhint) {
556 $fs->delete_area_files($context->id, 'question', 'hint', $oldhint->id);
557 $DB->delete_records('question_hints', array('id' => $oldhint->id));
562 * Can be used to {@link save_question_options()} to transfer the combined
563 * feedback fields from $formdata to $options.
564 * @param object $options the $question->options object being built.
565 * @param object $formdata the data from the form.
566 * @param object $context the context the quetsion is being saved into.
567 * @param bool $withparts whether $options->shownumcorrect should be set.
569 protected function save_combined_feedback_helper($options, $formdata,
570 $context, $withparts = false) {
571 $options->correctfeedback = $this->import_or_save_files($formdata->correctfeedback,
572 $context, 'question', 'correctfeedback', $formdata->id);
573 $options->correctfeedbackformat = $formdata->correctfeedback['format'];
575 $options->partiallycorrectfeedback = $this->import_or_save_files(
576 $formdata->partiallycorrectfeedback,
577 $context, 'question', 'partiallycorrectfeedback', $formdata->id);
578 $options->partiallycorrectfeedbackformat = $formdata->partiallycorrectfeedback['format'];
580 $options->incorrectfeedback = $this->import_or_save_files($formdata->incorrectfeedback,
581 $context, 'question', 'incorrectfeedback', $formdata->id);
582 $options->incorrectfeedbackformat = $formdata->incorrectfeedback['format'];
584 if ($withparts) {
585 $options->shownumcorrect = !empty($formdata->shownumcorrect);
588 return $options;
592 * Loads the question type specific options for the question.
594 * This function loads any question type specific options for the
595 * question from the database into the question object. This information
596 * is placed in the $question->options field. A question type is
597 * free, however, to decide on a internal structure of the options field.
598 * @return bool Indicates success or failure.
599 * @param object $question The question object for the question. This object
600 * should be updated to include the question type
601 * specific information (it is passed by reference).
603 public function get_question_options($question) {
604 global $CFG, $DB, $OUTPUT;
606 if (!isset($question->options)) {
607 $question->options = new stdClass();
610 $extraquestionfields = $this->extra_question_fields();
611 if (is_array($extraquestionfields)) {
612 $question_extension_table = array_shift($extraquestionfields);
613 $extra_data = $DB->get_record($question_extension_table,
614 array($this->questionid_column_name() => $question->id),
615 implode(', ', $extraquestionfields));
616 if ($extra_data) {
617 foreach ($extraquestionfields as $field) {
618 $question->options->$field = $extra_data->$field;
620 } else {
621 echo $OUTPUT->notification('Failed to load question options from the table ' .
622 $question_extension_table . ' for questionid ' . $question->id);
623 return false;
627 $extraanswerfields = $this->extra_answer_fields();
628 if (is_array($extraanswerfields)) {
629 $answer_extension_table = array_shift($extraanswerfields);
630 $question->options->answers = $DB->get_records_sql("
631 SELECT qa.*, qax." . implode(', qax.', $extraanswerfields) . "
632 FROM {question_answers} qa, {{$answer_extension_table}} qax
633 WHERE qa.question = ? AND qax.answerid = qa.id
634 ORDER BY qa.id", array($question->id));
635 if (!$question->options->answers) {
636 echo $OUTPUT->notification('Failed to load question answers from the table ' .
637 $answer_extension_table . 'for questionid ' . $question->id);
638 return false;
640 } else {
641 // Don't check for success or failure because some question types do
642 // not use the answers table.
643 $question->options->answers = $DB->get_records('question_answers',
644 array('question' => $question->id), 'id ASC');
647 $question->hints = $DB->get_records('question_hints',
648 array('questionid' => $question->id), 'id ASC');
650 return true;
654 * Create an appropriate question_definition for the question of this type
655 * using data loaded from the database.
656 * @param object $questiondata the question data loaded from the database.
657 * @return question_definition the corresponding question_definition.
659 public function make_question($questiondata) {
660 $question = $this->make_question_instance($questiondata);
661 $this->initialise_question_instance($question, $questiondata);
662 return $question;
666 * Create an appropriate question_definition for the question of this type
667 * using data loaded from the database.
668 * @param object $questiondata the question data loaded from the database.
669 * @return question_definition an instance of the appropriate question_definition subclass.
670 * Still needs to be initialised.
672 protected function make_question_instance($questiondata) {
673 question_bank::load_question_definition_classes($this->name());
674 $class = 'qtype_' . $this->name() . '_question';
675 return new $class();
679 * Initialise the common question_definition fields.
680 * @param question_definition $question the question_definition we are creating.
681 * @param object $questiondata the question data loaded from the database.
683 protected function initialise_question_instance(question_definition $question, $questiondata) {
684 $question->id = $questiondata->id;
685 $question->category = $questiondata->category;
686 $question->contextid = $questiondata->contextid;
687 $question->parent = $questiondata->parent;
688 $question->qtype = $this;
689 $question->name = $questiondata->name;
690 $question->questiontext = $questiondata->questiontext;
691 $question->questiontextformat = $questiondata->questiontextformat;
692 $question->generalfeedback = $questiondata->generalfeedback;
693 $question->generalfeedbackformat = $questiondata->generalfeedbackformat;
694 $question->defaultmark = $questiondata->defaultmark + 0;
695 $question->length = $questiondata->length;
696 $question->penalty = $questiondata->penalty;
697 $question->stamp = $questiondata->stamp;
698 $question->version = $questiondata->version;
699 $question->hidden = $questiondata->hidden;
700 $question->timecreated = $questiondata->timecreated;
701 $question->timemodified = $questiondata->timemodified;
702 $question->createdby = $questiondata->createdby;
703 $question->modifiedby = $questiondata->modifiedby;
705 $this->initialise_question_hints($question, $questiondata);
709 * Initialise question_definition::hints field.
710 * @param question_definition $question the question_definition we are creating.
711 * @param object $questiondata the question data loaded from the database.
713 protected function initialise_question_hints(question_definition $question, $questiondata) {
714 if (empty($questiondata->hints)) {
715 return;
717 foreach ($questiondata->hints as $hint) {
718 $question->hints[] = $this->make_hint($hint);
723 * Create a question_hint, or an appropriate subclass for this question,
724 * from a row loaded from the database.
725 * @param object $hint the DB row from the question hints table.
726 * @return question_hint
728 protected function make_hint($hint) {
729 return question_hint::load_from_record($hint);
733 * Initialise the combined feedback fields.
734 * @param question_definition $question the question_definition we are creating.
735 * @param object $questiondata the question data loaded from the database.
736 * @param bool $withparts whether to set the shownumcorrect field.
738 protected function initialise_combined_feedback(question_definition $question,
739 $questiondata, $withparts = false) {
740 $question->correctfeedback = $questiondata->options->correctfeedback;
741 $question->correctfeedbackformat = $questiondata->options->correctfeedbackformat;
742 $question->partiallycorrectfeedback = $questiondata->options->partiallycorrectfeedback;
743 $question->partiallycorrectfeedbackformat =
744 $questiondata->options->partiallycorrectfeedbackformat;
745 $question->incorrectfeedback = $questiondata->options->incorrectfeedback;
746 $question->incorrectfeedbackformat = $questiondata->options->incorrectfeedbackformat;
747 if ($withparts) {
748 $question->shownumcorrect = $questiondata->options->shownumcorrect;
753 * Initialise question_definition::answers field.
754 * @param question_definition $question the question_definition we are creating.
755 * @param object $questiondata the question data loaded from the database.
756 * @param bool $forceplaintextanswers most qtypes assume that answers are
757 * FORMAT_PLAIN, and dont use the answerformat DB column (it contains
758 * the default 0 = FORMAT_MOODLE). Therefore, by default this method
759 * ingores answerformat. Pass false here to use answerformat. For example
760 * multichoice does this.
762 protected function initialise_question_answers(question_definition $question,
763 $questiondata, $forceplaintextanswers = true) {
764 $question->answers = array();
765 if (empty($questiondata->options->answers)) {
766 return;
768 foreach ($questiondata->options->answers as $a) {
769 $question->answers[$a->id] = new question_answer($a->id, $a->answer,
770 $a->fraction, $a->feedback, $a->feedbackformat);
771 if (!$forceplaintextanswers) {
772 $question->answers[$a->id]->answerformat = $a->answerformat;
778 * Deletes the question-type specific data when a question is deleted.
779 * @param int $question the question being deleted.
780 * @param int $contextid the context this quesiotn belongs to.
782 public function delete_question($questionid, $contextid) {
783 global $DB;
785 $this->delete_files($questionid, $contextid);
787 $extraquestionfields = $this->extra_question_fields();
788 if (is_array($extraquestionfields)) {
789 $question_extension_table = array_shift($extraquestionfields);
790 $DB->delete_records($question_extension_table,
791 array($this->questionid_column_name() => $questionid));
794 $extraanswerfields = $this->extra_answer_fields();
795 if (is_array($extraanswerfields)) {
796 $answer_extension_table = array_shift($extraanswerfields);
797 $DB->delete_records_select($answer_extension_table,
798 'answerid IN (SELECT qa.id FROM {question_answers} qa WHERE qa.question = ?)',
799 array($questionid));
802 $DB->delete_records('question_answers', array('question' => $questionid));
804 $DB->delete_records('question_hints', array('questionid' => $questionid));
808 * Returns the number of question numbers which are used by the question
810 * This function returns the number of question numbers to be assigned
811 * to the question. Most question types will have length one; they will be
812 * assigned one number. The 'description' type, however does not use up a
813 * number and so has a length of zero. Other question types may wish to
814 * handle a bundle of questions and hence return a number greater than one.
815 * @return int The number of question numbers which should be
816 * assigned to the question.
817 * @param object $question The question whose length is to be determined.
818 * Question type specific information is included.
820 public function actual_number_of_questions($question) {
821 // By default, each question is given one number
822 return 1;
826 * @param object $question
827 * @return number|null either a fraction estimating what the student would
828 * score by guessing, or null, if it is not possible to estimate.
830 public function get_random_guess_score($questiondata) {
831 return 0;
835 * This method should return all the possible types of response that are
836 * recognised for this question.
838 * The question is modelled as comprising one or more subparts. For each
839 * subpart, there are one or more classes that that students response
840 * might fall into, each of those classes earning a certain score.
842 * For example, in a shortanswer question, there is only one subpart, the
843 * text entry field. The response the student gave will be classified according
844 * to which of the possible $question->options->answers it matches.
846 * For the matching question type, there will be one subpart for each
847 * question stem, and for each stem, each of the possible choices is a class
848 * of student's response.
850 * A response is an object with two fields, ->responseclass is a string
851 * presentation of that response, and ->fraction, the credit for a response
852 * in that class.
854 * Array keys have no specific meaning, but must be unique, and must be
855 * the same if this function is called repeatedly.
857 * @param object $question the question definition data.
858 * @return array keys are subquestionid, values are arrays of possible
859 * responses to that subquestion.
861 public function get_possible_responses($questiondata) {
862 return array();
866 * Like @see{get_html_head_contributions}, but this method is for CSS and
867 * JavaScript required on the question editing page question/question.php.
869 public function get_editing_head_contributions() {
870 // By default, we link to any of the files styles.css, styles.php,
871 // script.js or script.php that exist in the plugin folder.
872 // Core question types should not use this mechanism. Their styles
873 // should be included in the standard theme.
874 $this->find_standard_scripts();
878 * Utility method used by @see{get_html_head_contributions} and
879 * @see{get_editing_head_contributions}. This looks for any of the files
880 * script.js or script.php that exist in the plugin folder and ensures they
881 * get included.
883 public function find_standard_scripts() {
884 global $PAGE;
886 $plugindir = $this->plugin_dir();
887 $plugindirrel = 'question/type/' . $this->name();
889 if (file_exists($plugindir . '/script.js')) {
890 $PAGE->requires->js('/' . $plugindirrel . '/script.js');
892 if (file_exists($plugindir . '/script.php')) {
893 $PAGE->requires->js('/' . $plugindirrel . '/script.php');
898 * Returns true if the editing wizard is finished, false otherwise.
900 * The default implementation returns true, which is suitable for all question-
901 * types that only use one editing form. This function is used in
902 * question.php to decide whether we can regrade any states of the edited
903 * question and redirect to edit.php.
905 * The dataset dependent question-type, which is extended by the calculated
906 * question-type, overwrites this method because it uses multiple pages (i.e.
907 * a wizard) to set up the question and associated datasets.
909 * @param object $form The data submitted by the previous page.
911 * @return bool Whether the wizard's last page was submitted or not.
913 public function finished_edit_wizard($form) {
914 //In the default case there is only one edit page.
915 return true;
918 /// IMPORT/EXPORT FUNCTIONS /////////////////
921 * Imports question from the Moodle XML format
923 * Imports question using information from extra_question_fields function
924 * If some of you fields contains id's you'll need to reimplement this
926 public function import_from_xml($data, $question, $format, $extra=null) {
927 $question_type = $data['@']['type'];
928 if ($question_type != $this->name()) {
929 return false;
932 $extraquestionfields = $this->extra_question_fields();
933 if (!is_array($extraquestionfields)) {
934 return false;
937 //omit table name
938 array_shift($extraquestionfields);
939 $qo = $format->import_headers($data);
940 $qo->qtype = $question_type;
942 foreach ($extraquestionfields as $field) {
943 $qo->$field = $format->getpath($data, array('#', $field, 0, '#'), $qo->$field);
946 // run through the answers
947 $answers = $data['#']['answer'];
948 $a_count = 0;
949 $extraasnwersfields = $this->extra_answer_fields();
950 if (is_array($extraasnwersfields)) {
951 // TODO import the answers, with any extra data.
952 } else {
953 foreach ($answers as $answer) {
954 $ans = $format->import_answer($answer);
955 $qo->answer[$a_count] = $ans->answer;
956 $qo->fraction[$a_count] = $ans->fraction;
957 $qo->feedback[$a_count] = $ans->feedback;
958 ++$a_count;
961 return $qo;
965 * Export question to the Moodle XML format
967 * Export question using information from extra_question_fields function
968 * If some of you fields contains id's you'll need to reimplement this
970 public function export_to_xml($question, $format, $extra=null) {
971 $extraquestionfields = $this->extra_question_fields();
972 if (!is_array($extraquestionfields)) {
973 return false;
976 //omit table name
977 array_shift($extraquestionfields);
978 $expout='';
979 foreach ($extraquestionfields as $field) {
980 $exportedvalue = $question->options->$field;
981 if (!empty($exportedvalue) && htmlspecialchars($exportedvalue) != $exportedvalue) {
982 $exportedvalue = '<![CDATA[' . $exportedvalue . ']]>';
984 $expout .= " <$field>{$exportedvalue}</$field>\n";
987 $extraasnwersfields = $this->extra_answer_fields();
988 if (is_array($extraasnwersfields)) {
989 // TODO export answers with any extra data
990 } else {
991 foreach ($question->options->answers as $answer) {
992 $percent = 100 * $answer->fraction;
993 $expout .= " <answer fraction=\"$percent\">\n";
994 $expout .= $format->writetext($answer->answer, 3, false);
995 $expout .= " <feedback>\n";
996 $expout .= $format->writetext($answer->feedback, 4, false);
997 $expout .= " </feedback>\n";
998 $expout .= " </answer>\n";
1001 return $expout;
1005 * Abstract function implemented by each question type. It runs all the code
1006 * required to set up and save a question of any type for testing purposes.
1007 * Alternate DB table prefix may be used to facilitate data deletion.
1009 public function generate_test($name, $courseid=null) {
1010 $form = new stdClass();
1011 $form->name = $name;
1012 $form->questiontextformat = 1;
1013 $form->questiontext = 'test question, generated by script';
1014 $form->defaultmark = 1;
1015 $form->penalty = 0.3333333;
1016 $form->generalfeedback = "Well done";
1018 $context = get_context_instance(CONTEXT_COURSE, $courseid);
1019 $newcategory = question_make_default_categories(array($context));
1020 $form->category = $newcategory->id . ',1';
1022 $question = new stdClass();
1023 $question->courseid = $courseid;
1024 $question->qtype = $this->qtype;
1025 return array($form, $question);
1029 * Get question context by category id
1030 * @param int $category
1031 * @return object $context
1033 protected function get_context_by_category_id($category) {
1034 global $DB;
1035 $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$category));
1036 $context = get_context_instance_by_id($contextid);
1037 return $context;
1041 * Save the file belonging to one text field.
1043 * @param array $field the data from the form (or from import). This will
1044 * normally have come from the formslib editor element, so it will be an
1045 * array with keys 'text', 'format' and 'itemid'. However, when we are
1046 * importing, it will be an array with keys 'text', 'format' and 'files'
1047 * @param object $context the context the question is in.
1048 * @param string $component indentifies the file area question.
1049 * @param string $filearea indentifies the file area questiontext,
1050 * generalfeedback, answerfeedback, etc.
1051 * @param int $itemid identifies the file area.
1053 * @return string the text for this field, after files have been processed.
1055 protected function import_or_save_files($field, $context, $component, $filearea, $itemid) {
1056 if (!empty($field['itemid'])) {
1057 // This is the normal case. We are safing the questions editing form.
1058 return file_save_draft_area_files($field['itemid'], $context->id, $component,
1059 $filearea, $itemid, $this->fileoptions, trim($field['text']));
1061 } else if (!empty($field['files'])) {
1062 // This is the case when we are doing an import.
1063 foreach ($field['files'] as $file) {
1064 $this->import_file($context, $component, $filearea, $itemid, $file);
1067 return trim($field['text']);
1071 * Move all the files belonging to this question from one context to another.
1072 * @param int $questionid the question being moved.
1073 * @param int $oldcontextid the context it is moving from.
1074 * @param int $newcontextid the context it is moving to.
1076 public function move_files($questionid, $oldcontextid, $newcontextid) {
1077 $fs = get_file_storage();
1078 $fs->move_area_files_to_new_context($oldcontextid,
1079 $newcontextid, 'question', 'questiontext', $questionid);
1080 $fs->move_area_files_to_new_context($oldcontextid,
1081 $newcontextid, 'question', 'generalfeedback', $questionid);
1085 * Move all the files belonging to this question's answers when the question
1086 * is moved from one context to another.
1087 * @param int $questionid the question being moved.
1088 * @param int $oldcontextid the context it is moving from.
1089 * @param int $newcontextid the context it is moving to.
1090 * @param bool $answerstoo whether there is an 'answer' question area,
1091 * as well as an 'answerfeedback' one. Default false.
1093 protected function move_files_in_answers($questionid, $oldcontextid,
1094 $newcontextid, $answerstoo = false) {
1095 global $DB;
1096 $fs = get_file_storage();
1098 $answerids = $DB->get_records_menu('question_answers',
1099 array('question' => $questionid), 'id', 'id,1');
1100 foreach ($answerids as $answerid => $notused) {
1101 if ($answerstoo) {
1102 $fs->move_area_files_to_new_context($oldcontextid,
1103 $newcontextid, 'question', 'answer', $answerid);
1105 $fs->move_area_files_to_new_context($oldcontextid,
1106 $newcontextid, 'question', 'answerfeedback', $answerid);
1111 * Delete all the files belonging to this question.
1112 * @param int $questionid the question being deleted.
1113 * @param int $contextid the context the question is in.
1115 protected function delete_files($questionid, $contextid) {
1116 $fs = get_file_storage();
1117 $fs->delete_area_files($contextid, 'question', 'questiontext', $questionid);
1118 $fs->delete_area_files($contextid, 'question', 'generalfeedback', $questionid);
1122 * Delete all the files belonging to this question's answers.
1123 * @param int $questionid the question being deleted.
1124 * @param int $contextid the context the question is in.
1125 * @param bool $answerstoo whether there is an 'answer' question area,
1126 * as well as an 'answerfeedback' one. Default false.
1128 protected function delete_files_in_answers($questionid, $contextid, $answerstoo = false) {
1129 global $DB;
1130 $fs = get_file_storage();
1132 $answerids = $DB->get_records_menu('question_answers',
1133 array('question' => $questionid), 'id', 'id,1');
1134 foreach ($answerids as $answerid => $notused) {
1135 if ($answerstoo) {
1136 $fs->delete_area_files($contextid, 'question', 'answer', $answerid);
1138 $fs->delete_area_files($contextid, 'question', 'answerfeedback', $answerid);
1142 public function import_file($context, $component, $filearea, $itemid, $file) {
1143 $fs = get_file_storage();
1144 $record = new stdClass();
1145 if (is_object($context)) {
1146 $record->contextid = $context->id;
1147 } else {
1148 $record->contextid = $context;
1150 $record->component = $component;
1151 $record->filearea = $filearea;
1152 $record->itemid = $itemid;
1153 $record->filename = $file->name;
1154 $record->filepath = '/';
1155 return $fs->create_file_from_string($record, $this->decode_file($file));
1158 protected function decode_file($file) {
1159 switch ($file->encoding) {
1160 case 'base64':
1161 default:
1162 return base64_decode($file->content);
1169 * This class is used in the return value from
1170 * {@link question_type::get_possible_responses()}.
1172 * @copyright 2010 The Open University
1173 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1175 class question_possible_response {
1177 * @var string the classification of this response the student gave to this
1178 * part of the question. Must match one of the responseclasses returned by
1179 * {@link question_type::get_possible_responses()}.
1181 public $responseclass;
1182 /** @var string the actual response the student gave to this part. */
1183 public $fraction;
1185 * Constructor, just an easy way to set the fields.
1186 * @param string $responseclassid see the field descriptions above.
1187 * @param string $response see the field descriptions above.
1188 * @param number $fraction see the field descriptions above.
1190 public function __construct($responseclass, $fraction) {
1191 $this->responseclass = $responseclass;
1192 $this->fraction = $fraction;
1195 public static function no_response() {
1196 return new question_possible_response(get_string('noresponse', 'question'), 0);