MDL-61837 core_question: replace tag fields on tag modal
[moodle.git] / question / type / questiontypebase.php
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.
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/>.
17 /**
18  * The default questiontype class.
19  *
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
24  */
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.
34  *
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.
37  *
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.
44  *
45  * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
46  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
47  */
48 class question_type {
49     protected $fileoptions = array(
50         'subdirs' => true,
51         'maxfiles' => -1,
52         'maxbytes' => 0,
53     );
55     public function __construct() {
56     }
58     /**
59      * @return string the name of this question type.
60      */
61     public function name() {
62         return substr(get_class($this), 6);
63     }
65     /**
66      * @return string the full frankenstyle name for this plugin.
67      */
68     public function plugin_name() {
69         return get_class($this);
70     }
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('pluginname', $this->plugin_name());
78     }
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.
85      *
86      * @return mixed the desired string, or false to hide this question type in the menu.
87      */
88     public function menu_name() {
89         return $this->local_name();
90     }
92     /**
93      * @return bool override this to return false if this is not really a
94      *      question type, for example the description question type is not
95      *      really a question type.
96      */
97     public function is_real_question_type() {
98         return true;
99     }
101     /**
102      * @return bool true if this question type sometimes requires manual grading.
103      */
104     public function is_manual_graded() {
105         return false;
106     }
108     /**
109      * @param object $question a question of this type.
110      * @param string $otherquestionsinuse comma-separate list of other question ids in this attempt.
111      * @return bool true if a particular instance of this question requires manual grading.
112      */
113     public function is_question_manual_graded($question, $otherquestionsinuse) {
114         return $this->is_manual_graded();
115     }
117     /**
118      * @return bool true if this question type can be used by the random question type.
119      */
120     public function is_usable_by_random() {
121         return true;
122     }
124     /**
125      * Whether this question type can perform a frequency analysis of student
126      * responses.
127      *
128      * If this method returns true, you must implement the get_possible_responses
129      * method, and the question_definition class must implement the
130      * classify_response method.
131      *
132      * @return bool whether this report can analyse all the student responses
133      * for things like the quiz statistics report.
134      */
135     public function can_analyse_responses() {
136         // This works in most cases.
137         return !$this->is_manual_graded();
138     }
140     /**
141      * @return whether the question_answers.answer field needs to have
142      * restore_decode_content_links_worker called on it.
143      */
144     public function has_html_answers() {
145         return false;
146     }
148     /**
149      * If your question type has a table that extends the question table, and
150      * you want the base class to automatically save, backup and restore the extra fields,
151      * override this method to return an array wherer the first element is the table name,
152      * and the subsequent entries are the column names (apart from id and questionid).
153      *
154      * @return mixed array as above, or null to tell the base class to do nothing.
155      */
156     public function extra_question_fields() {
157         return null;
158     }
160     /**
161      * If you use extra_question_fields, overload this function to return question id field name
162      *  in case you table use another name for this column
163      */
164     public function questionid_column_name() {
165         return 'questionid';
166     }
168     /**
169      * If your question type has a table that extends the question_answers table,
170      * make this method return an array wherer the first element is the table name,
171      * and the subsequent entries are the column names (apart from id and answerid).
172      *
173      * @return mixed array as above, or null to tell the base class to do nothing.
174      */
175     public function extra_answer_fields() {
176         return null;
177     }
179     /**
180      * If the quetsion type uses files in responses, then this method should
181      * return an array of all the response variables that might have corresponding
182      * files. For example, the essay qtype returns array('attachments', 'answers').
183      *
184      * @return array response variable names that may have associated files.
185      */
186     public function response_file_areas() {
187         return array();
188     }
190     /**
191      * Return an instance of the question editing form definition. This looks for a
192      * class called edit_{$this->name()}_question_form in the file
193      * question/type/{$this->name()}/edit_{$this->name()}_question_form.php
194      * and if it exists returns an instance of it.
195      *
196      * @param string $submiturl passed on to the constructor call.
197      * @return object an instance of the form definition, or null if one could not be found.
198      */
199     public function create_editing_form($submiturl, $question, $category,
200             $contexts, $formeditable) {
201         global $CFG;
202         require_once($CFG->dirroot . '/question/type/edit_question_form.php');
203         $definitionfile = $CFG->dirroot . '/question/type/' . $this->name() .
204                 '/edit_' . $this->name() . '_form.php';
205         if (!is_readable($definitionfile) || !is_file($definitionfile)) {
206             throw new coding_exception($this->plugin_name() .
207                     ' is missing the definition of its editing formin file ' .
208                     $definitionfile . '.');
209         }
210         require_once($definitionfile);
211         $classname = $this->plugin_name() . '_edit_form';
212         if (!class_exists($classname)) {
213             throw new coding_exception($this->plugin_name() .
214                     ' does not define the class ' . $this->plugin_name() .
215                     '_edit_form.');
216         }
217         return new $classname($submiturl, $question, $category, $contexts, $formeditable);
218     }
220     /**
221      * @return string the full path of the folder this plugin's files live in.
222      */
223     public function plugin_dir() {
224         global $CFG;
225         return $CFG->dirroot . '/question/type/' . $this->name();
226     }
228     /**
229      * @return string the URL of the folder this plugin's files live in.
230      */
231     public function plugin_baseurl() {
232         global $CFG;
233         return $CFG->wwwroot . '/question/type/' . $this->name();
234     }
236     /**
237      * This method should be overriden if you want to include a special heading or some other
238      * html on a question editing page besides the question editing form.
239      *
240      * @param question_edit_form $mform a child of question_edit_form
241      * @param object $question
242      * @param string $wizardnow is '' for first page.
243      */
244     public function display_question_editing_page($mform, $question, $wizardnow) {
245         global $OUTPUT;
246         $heading = $this->get_heading(empty($question->id));
247         echo $OUTPUT->heading_with_help($heading, 'pluginname', $this->plugin_name());
248         $mform->display();
249     }
251     /**
252      * Method called by display_question_editing_page and by question.php to get
253      * heading for breadcrumbs.
254      *
255      * @return string the heading
256      */
257     public function get_heading($adding = false) {
258         if ($adding) {
259             $string = 'pluginnameadding';
260         } else {
261             $string = 'pluginnameediting';
262         }
263         return get_string($string, $this->plugin_name());
264     }
266     /**
267      * Set any missing settings for this question to the default values. This is
268      * called before displaying the question editing form.
269      *
270      * @param object $questiondata the question data, loaded from the databsae,
271      *      or more likely a newly created question object that is only partially
272      *      initialised.
273      */
274     public function set_default_options($questiondata) {
275     }
277     /**
278      * Saves (creates or updates) a question.
279      *
280      * Given some question info and some data about the answers
281      * this function parses, organises and saves the question
282      * It is used by {@link question.php} when saving new data from
283      * a form, and also by {@link import.php} when importing questions
284      * This function in turn calls {@link save_question_options}
285      * to save question-type specific data.
286      *
287      * Whether we are saving a new question or updating an existing one can be
288      * determined by testing !empty($question->id). If it is not empty, we are updating.
289      *
290      * The question will be saved in category $form->category.
291      *
292      * @param object $question the question object which should be updated. For a
293      *      new question will be mostly empty.
294      * @param object $form the object containing the information to save, as if
295      *      from the question editing form.
296      * @param object $course not really used any more.
297      * @return object On success, return the new question object. On failure,
298      *       return an object as follows. If the error object has an errors field,
299      *       display that as an error message. Otherwise, the editing form will be
300      *       redisplayed with validation errors, from validation_errors field, which
301      *       is itself an object, shown next to the form fields. (I don't think this
302      *       is accurate any more.)
303      */
304     public function save_question($question, $form) {
305         global $USER, $DB, $OUTPUT;
307         list($question->category) = explode(',', $form->category);
308         $context = $this->get_context_by_category_id($question->category);
310         // This default implementation is suitable for most
311         // question types.
313         // First, save the basic question itself.
314         $question->name = trim($form->name);
315         $question->parent = isset($form->parent) ? $form->parent : 0;
316         $question->length = $this->actual_number_of_questions($question);
317         $question->penalty = isset($form->penalty) ? $form->penalty : 0;
319         // The trim call below has the effect of casting any strange values received,
320         // like null or false, to an appropriate string, so we only need to test for
321         // missing values. Be careful not to break the value '0' here.
322         if (!isset($form->questiontext['text'])) {
323             $question->questiontext = '';
324         } else {
325             $question->questiontext = trim($form->questiontext['text']);
326         }
327         $question->questiontextformat = !empty($form->questiontext['format']) ?
328                 $form->questiontext['format'] : 0;
330         if (empty($form->generalfeedback['text'])) {
331             $question->generalfeedback = '';
332         } else {
333             $question->generalfeedback = trim($form->generalfeedback['text']);
334         }
335         $question->generalfeedbackformat = !empty($form->generalfeedback['format']) ?
336                 $form->generalfeedback['format'] : 0;
338         if ($question->name === '') {
339             $question->name = shorten_text(strip_tags($form->questiontext['text']), 15);
340             if ($question->name === '') {
341                 $question->name = '-';
342             }
343         }
345         if ($question->penalty > 1 or $question->penalty < 0) {
346             $question->errors['penalty'] = get_string('invalidpenalty', 'question');
347         }
349         if (isset($form->defaultmark)) {
350             $question->defaultmark = $form->defaultmark;
351         }
353         // If the question is new, create it.
354         if (empty($question->id)) {
355             // Set the unique code.
356             $question->stamp = make_unique_id_code();
357             $question->createdby = $USER->id;
358             $question->timecreated = time();
359             $question->id = $DB->insert_record('question', $question);
360         }
362         // Now, whether we are updating a existing question, or creating a new
363         // one, we have to do the files processing and update the record.
364         // Question already exists, update.
365         $question->modifiedby = $USER->id;
366         $question->timemodified = time();
368         if (!empty($question->questiontext) && !empty($form->questiontext['itemid'])) {
369             $question->questiontext = file_save_draft_area_files($form->questiontext['itemid'],
370                     $context->id, 'question', 'questiontext', (int)$question->id,
371                     $this->fileoptions, $question->questiontext);
372         }
373         if (!empty($question->generalfeedback) && !empty($form->generalfeedback['itemid'])) {
374             $question->generalfeedback = file_save_draft_area_files(
375                     $form->generalfeedback['itemid'], $context->id,
376                     'question', 'generalfeedback', (int)$question->id,
377                     $this->fileoptions, $question->generalfeedback);
378         }
379         $DB->update_record('question', $question);
381         // Now to save all the answers and type-specific options.
382         $form->id = $question->id;
383         $form->qtype = $question->qtype;
384         $form->category = $question->category;
385         $form->questiontext = $question->questiontext;
386         $form->questiontextformat = $question->questiontextformat;
387         // Current context.
388         $form->context = $context;
390         $result = $this->save_question_options($form);
392         if (!empty($result->error)) {
393             print_error($result->error);
394         }
396         if (!empty($result->notice)) {
397             notice($result->notice, "question.php?id={$question->id}");
398         }
400         if (!empty($result->noticeyesno)) {
401             throw new coding_exception(
402                     '$result->noticeyesno no longer supported in save_question.');
403         }
405         // Give the question a unique version stamp determined by question_hash().
406         $DB->set_field('question', 'version', question_hash($question),
407                 array('id' => $question->id));
409         return $question;
410     }
412     /**
413      * Saves question-type specific options
414      *
415      * This is called by {@link save_question()} to save the question-type specific data
416      * @return object $result->error or $result->notice
417      * @param object $question  This holds the information from the editing form,
418      *      it is not a standard question object.
419      */
420     public function save_question_options($question) {
421         global $DB;
422         $extraquestionfields = $this->extra_question_fields();
424         if (is_array($extraquestionfields)) {
425             $question_extension_table = array_shift($extraquestionfields);
427             $function = 'update_record';
428             $questionidcolname = $this->questionid_column_name();
429             $options = $DB->get_record($question_extension_table,
430                     array($questionidcolname => $question->id));
431             if (!$options) {
432                 $function = 'insert_record';
433                 $options = new stdClass();
434                 $options->$questionidcolname = $question->id;
435             }
436             foreach ($extraquestionfields as $field) {
437                 if (property_exists($question, $field)) {
438                     $options->$field = $question->$field;
439                 }
440             }
442             $DB->{$function}($question_extension_table, $options);
443         }
444     }
446     /**
447      * Save the answers, with any extra data.
448      *
449      * Questions that use answers will call it from {@link save_question_options()}.
450      * @param object $question  This holds the information from the editing form,
451      *      it is not a standard question object.
452      * @return object $result->error or $result->notice
453      */
454     public function save_question_answers($question) {
455         global $DB;
457         $context = $question->context;
458         $oldanswers = $DB->get_records('question_answers',
459                 array('question' => $question->id), 'id ASC');
461         // We need separate arrays for answers and extra answer data, so no JOINS there.
462         $extraanswerfields = $this->extra_answer_fields();
463         $isextraanswerfields = is_array($extraanswerfields);
464         $extraanswertable = '';
465         $oldanswerextras = array();
466         if ($isextraanswerfields) {
467             $extraanswertable = array_shift($extraanswerfields);
468             if (!empty($oldanswers)) {
469                 $oldanswerextras = $DB->get_records_sql("SELECT * FROM {{$extraanswertable}} WHERE " .
470                     'answerid IN (SELECT id FROM {question_answers} WHERE question = ' . $question->id . ')' );
471             }
472         }
474         // Insert all the new answers.
475         foreach ($question->answer as $key => $answerdata) {
476             // Check for, and ignore, completely blank answer from the form.
477             if ($this->is_answer_empty($question, $key)) {
478                 continue;
479             }
481             // Update an existing answer if possible.
482             $answer = array_shift($oldanswers);
483             if (!$answer) {
484                 $answer = new stdClass();
485                 $answer->question = $question->id;
486                 $answer->answer = '';
487                 $answer->feedback = '';
488                 $answer->id = $DB->insert_record('question_answers', $answer);
489             }
491             $answer = $this->fill_answer_fields($answer, $question, $key, $context);
492             $DB->update_record('question_answers', $answer);
494             if ($isextraanswerfields) {
495                 // Check, if this answer contains some extra field data.
496                 if ($this->is_extra_answer_fields_empty($question, $key)) {
497                     continue;
498                 }
500                 $answerextra = array_shift($oldanswerextras);
501                 if (!$answerextra) {
502                     $answerextra = new stdClass();
503                     $answerextra->answerid = $answer->id;
504                     // Avoid looking for correct default for any possible DB field type
505                     // by setting real values.
506                     $answerextra = $this->fill_extra_answer_fields($answerextra, $question, $key, $context, $extraanswerfields);
507                     $answerextra->id = $DB->insert_record($extraanswertable, $answerextra);
508                 } else {
509                     // Update answerid, as record may be reused from another answer.
510                     $answerextra->answerid = $answer->id;
511                     $answerextra = $this->fill_extra_answer_fields($answerextra, $question, $key, $context, $extraanswerfields);
512                     $DB->update_record($extraanswertable, $answerextra);
513                 }
514             }
515         }
517         if ($isextraanswerfields) {
518             // Delete any left over extra answer fields records.
519             $oldanswerextraids = array();
520             foreach ($oldanswerextras as $oldextra) {
521                 $oldanswerextraids[] = $oldextra->id;
522             }
523             $DB->delete_records_list($extraanswertable, 'id', $oldanswerextraids);
524         }
526         // Delete any left over old answer records.
527         $fs = get_file_storage();
528         foreach ($oldanswers as $oldanswer) {
529             $fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id);
530             $DB->delete_records('question_answers', array('id' => $oldanswer->id));
531         }
532     }
534     /**
535      * Returns true is answer with the $key is empty in the question data and should not be saved in DB.
536      *
537      * The questions using question_answers table may want to overload this. Default code will work
538      * for shortanswer and similar question types.
539      * @param object $questiondata This holds the information from the question editing form or import.
540      * @param int $key A key of the answer in question.
541      * @return bool True if answer shouldn't be saved in DB.
542      */
543     protected function is_answer_empty($questiondata, $key) {
544         return trim($questiondata->answer[$key]) == '' && $questiondata->fraction[$key] == 0 &&
545                     html_is_blank($questiondata->feedback[$key]['text']);
546     }
548     /**
549      * Return $answer, filling necessary fields for the question_answers table.
550      *
551      * The questions using question_answers table may want to overload this. Default code will work
552      * for shortanswer and similar question types.
553      * @param stdClass $answer Object to save data.
554      * @param object $questiondata This holds the information from the question editing form or import.
555      * @param int $key A key of the answer in question.
556      * @param object $context needed for working with files.
557      * @return $answer answer with filled data.
558      */
559     protected function fill_answer_fields($answer, $questiondata, $key, $context) {
560         $answer->answer   = $questiondata->answer[$key];
561         $answer->fraction = $questiondata->fraction[$key];
562         $answer->feedback = $this->import_or_save_files($questiondata->feedback[$key],
563                 $context, 'question', 'answerfeedback', $answer->id);
564         $answer->feedbackformat = $questiondata->feedback[$key]['format'];
565         return $answer;
566     }
568     /**
569      * Returns true if extra answer fields for answer with the $key is empty
570      * in the question data and should not be saved in DB.
571      *
572      * Questions where extra answer fields are optional will want to overload this.
573      * @param object $questiondata This holds the information from the question editing form or import.
574      * @param int $key A key of the answer in question.
575      * @return bool True if extra answer data shouldn't be saved in DB.
576      */
577     protected function is_extra_answer_fields_empty($questiondata, $key) {
578         // No extra answer data in base class.
579         return true;
580     }
582     /**
583      * Return $answerextra, filling necessary fields for the extra answer fields table.
584      *
585      * The questions may want to overload it to save files or do other data processing.
586      * @param stdClass $answerextra Object to save data.
587      * @param object $questiondata This holds the information from the question editing form or import.
588      * @param int $key A key of the answer in question.
589      * @param object $context needed for working with files.
590      * @param array $extraanswerfields extra answer fields (without table name).
591      * @return $answer answerextra with filled data.
592      */
593     protected function fill_extra_answer_fields($answerextra, $questiondata, $key, $context, $extraanswerfields) {
594         foreach ($extraanswerfields as $field) {
595             // The $questiondata->$field[$key] won't work in PHP, break it down to two strings of code.
596             $fieldarray = $questiondata->$field;
597             $answerextra->$field = $fieldarray[$key];
598         }
599         return $answerextra;
600     }
602     public function save_hints($formdata, $withparts = false) {
603         global $DB;
604         $context = $formdata->context;
606         $oldhints = $DB->get_records('question_hints',
607                 array('questionid' => $formdata->id), 'id ASC');
610         $numhints = $this->count_hints_on_form($formdata, $withparts);
612         for ($i = 0; $i < $numhints; $i += 1) {
613             if (html_is_blank($formdata->hint[$i]['text'])) {
614                 $formdata->hint[$i]['text'] = '';
615             }
617             if ($withparts) {
618                 $clearwrong = !empty($formdata->hintclearwrong[$i]);
619                 $shownumcorrect = !empty($formdata->hintshownumcorrect[$i]);
620             }
622             if ($this->is_hint_empty_in_form_data($formdata, $i, $withparts)) {
623                 continue;
624             }
626             // Update an existing hint if possible.
627             $hint = array_shift($oldhints);
628             if (!$hint) {
629                 $hint = new stdClass();
630                 $hint->questionid = $formdata->id;
631                 $hint->hint = '';
632                 $hint->id = $DB->insert_record('question_hints', $hint);
633             }
635             $hint->hint = $this->import_or_save_files($formdata->hint[$i],
636                     $context, 'question', 'hint', $hint->id);
637             $hint->hintformat = $formdata->hint[$i]['format'];
638             if ($withparts) {
639                 $hint->clearwrong = $clearwrong;
640                 $hint->shownumcorrect = $shownumcorrect;
641             }
642             $hint->options = $this->save_hint_options($formdata, $i, $withparts);
643             $DB->update_record('question_hints', $hint);
644         }
646         // Delete any remaining old hints.
647         $fs = get_file_storage();
648         foreach ($oldhints as $oldhint) {
649             $fs->delete_area_files($context->id, 'question', 'hint', $oldhint->id);
650             $DB->delete_records('question_hints', array('id' => $oldhint->id));
651         }
652     }
654     /**
655      * Count number of hints on the form.
656      * Overload if you use custom hint controls.
657      * @param object $formdata the data from the form.
658      * @param bool $withparts whether to take into account clearwrong and shownumcorrect options.
659      * @return int count of hints on the form.
660      */
661     protected function count_hints_on_form($formdata, $withparts) {
662         if (!empty($formdata->hint)) {
663             $numhints = max(array_keys($formdata->hint)) + 1;
664         } else {
665             $numhints = 0;
666         }
668         if ($withparts) {
669             if (!empty($formdata->hintclearwrong)) {
670                 $numclears = max(array_keys($formdata->hintclearwrong)) + 1;
671             } else {
672                 $numclears = 0;
673             }
674             if (!empty($formdata->hintshownumcorrect)) {
675                 $numshows = max(array_keys($formdata->hintshownumcorrect)) + 1;
676             } else {
677                 $numshows = 0;
678             }
679             $numhints = max($numhints, $numclears, $numshows);
680         }
681         return $numhints;
682     }
684     /**
685      * Determine if the hint with specified number is not empty and should be saved.
686      * Overload if you use custom hint controls.
687      * @param object $formdata the data from the form.
688      * @param int $number number of hint under question.
689      * @param bool $withparts whether to take into account clearwrong and shownumcorrect options.
690      * @return bool is this particular hint data empty.
691      */
692     protected function is_hint_empty_in_form_data($formdata, $number, $withparts) {
693         if ($withparts) {
694             return empty($formdata->hint[$number]['text']) && empty($formdata->hintclearwrong[$number]) &&
695                     empty($formdata->hintshownumcorrect[$number]);
696         } else {
697             return  empty($formdata->hint[$number]['text']);
698         }
699     }
701     /**
702      * Save additional question type data into the hint optional field.
703      * Overload if you use custom hint information.
704      * @param object $formdata the data from the form.
705      * @param int $number number of hint to get options from.
706      * @param bool $withparts whether question have parts.
707      * @return string value to save into the options field of question_hints table.
708      */
709     protected function save_hint_options($formdata, $number, $withparts) {
710         return null;    // By default, options field is unused.
711     }
713     /**
714      * Can be used to {@link save_question_options()} to transfer the combined
715      * feedback fields from $formdata to $options.
716      * @param object $options the $question->options object being built.
717      * @param object $formdata the data from the form.
718      * @param object $context the context the quetsion is being saved into.
719      * @param bool $withparts whether $options->shownumcorrect should be set.
720      */
721     protected function save_combined_feedback_helper($options, $formdata,
722             $context, $withparts = false) {
723         $options->correctfeedback = $this->import_or_save_files($formdata->correctfeedback,
724                 $context, 'question', 'correctfeedback', $formdata->id);
725         $options->correctfeedbackformat = $formdata->correctfeedback['format'];
727         $options->partiallycorrectfeedback = $this->import_or_save_files(
728                 $formdata->partiallycorrectfeedback,
729                 $context, 'question', 'partiallycorrectfeedback', $formdata->id);
730         $options->partiallycorrectfeedbackformat = $formdata->partiallycorrectfeedback['format'];
732         $options->incorrectfeedback = $this->import_or_save_files($formdata->incorrectfeedback,
733                 $context, 'question', 'incorrectfeedback', $formdata->id);
734         $options->incorrectfeedbackformat = $formdata->incorrectfeedback['format'];
736         if ($withparts) {
737             $options->shownumcorrect = !empty($formdata->shownumcorrect);
738         }
740         return $options;
741     }
743     /**
744      * Loads the question type specific options for the question.
745      *
746      * This function loads any question type specific options for the
747      * question from the database into the question object. This information
748      * is placed in the $question->options field. A question type is
749      * free, however, to decide on a internal structure of the options field.
750      * @return bool            Indicates success or failure.
751      * @param object $question The question object for the question. This object
752      *                         should be updated to include the question type
753      *                         specific information (it is passed by reference).
754      */
755     public function get_question_options($question) {
756         global $CFG, $DB, $OUTPUT;
758         if (!isset($question->options)) {
759             $question->options = new stdClass();
760         }
762         $extraquestionfields = $this->extra_question_fields();
763         if (is_array($extraquestionfields)) {
764             $question_extension_table = array_shift($extraquestionfields);
765             $extra_data = $DB->get_record($question_extension_table,
766                     array($this->questionid_column_name() => $question->id),
767                     implode(', ', $extraquestionfields));
768             if ($extra_data) {
769                 foreach ($extraquestionfields as $field) {
770                     $question->options->$field = $extra_data->$field;
771                 }
772             } else {
773                 echo $OUTPUT->notification('Failed to load question options from the table ' .
774                         $question_extension_table . ' for questionid ' . $question->id);
775                 return false;
776             }
777         }
779         $extraanswerfields = $this->extra_answer_fields();
780         if (is_array($extraanswerfields)) {
781             $answerextensiontable = array_shift($extraanswerfields);
782             // Use LEFT JOIN in case not every answer has extra data.
783             $question->options->answers = $DB->get_records_sql("
784                     SELECT qa.*, qax." . implode(', qax.', $extraanswerfields) . '
785                     FROM {question_answers} qa ' . "
786                     LEFT JOIN {{$answerextensiontable}} qax ON qa.id = qax.answerid
787                     WHERE qa.question = ?
788                     ORDER BY qa.id", array($question->id));
789             if (!$question->options->answers) {
790                 echo $OUTPUT->notification('Failed to load question answers from the table ' .
791                         $answerextensiontable . 'for questionid ' . $question->id);
792                 return false;
793             }
794         } else {
795             // Don't check for success or failure because some question types do
796             // not use the answers table.
797             $question->options->answers = $DB->get_records('question_answers',
798                     array('question' => $question->id), 'id ASC');
799         }
801         $question->hints = $DB->get_records('question_hints',
802                 array('questionid' => $question->id), 'id ASC');
804         return true;
805     }
807     /**
808      * Create an appropriate question_definition for the question of this type
809      * using data loaded from the database.
810      * @param object $questiondata the question data loaded from the database.
811      * @return question_definition the corresponding question_definition.
812      */
813     public function make_question($questiondata) {
814         $question = $this->make_question_instance($questiondata);
815         $this->initialise_question_instance($question, $questiondata);
816         return $question;
817     }
819     /**
820      * Create an appropriate question_definition for the question of this type
821      * using data loaded from the database.
822      * @param object $questiondata the question data loaded from the database.
823      * @return question_definition an instance of the appropriate question_definition subclass.
824      *      Still needs to be initialised.
825      */
826     protected function make_question_instance($questiondata) {
827         question_bank::load_question_definition_classes($this->name());
828         $class = 'qtype_' . $this->name() . '_question';
829         return new $class();
830     }
832     /**
833      * Initialise the common question_definition fields.
834      * @param question_definition $question the question_definition we are creating.
835      * @param object $questiondata the question data loaded from the database.
836      */
837     protected function initialise_question_instance(question_definition $question, $questiondata) {
838         $question->id = $questiondata->id;
839         $question->category = $questiondata->category;
840         $question->contextid = $questiondata->contextid;
841         $question->parent = $questiondata->parent;
842         $question->qtype = $this;
843         $question->name = $questiondata->name;
844         $question->questiontext = $questiondata->questiontext;
845         $question->questiontextformat = $questiondata->questiontextformat;
846         $question->generalfeedback = $questiondata->generalfeedback;
847         $question->generalfeedbackformat = $questiondata->generalfeedbackformat;
848         $question->defaultmark = $questiondata->defaultmark + 0;
849         $question->length = $questiondata->length;
850         $question->penalty = $questiondata->penalty;
851         $question->stamp = $questiondata->stamp;
852         $question->version = $questiondata->version;
853         $question->hidden = $questiondata->hidden;
854         $question->timecreated = $questiondata->timecreated;
855         $question->timemodified = $questiondata->timemodified;
856         $question->createdby = $questiondata->createdby;
857         $question->modifiedby = $questiondata->modifiedby;
859         // Fill extra question fields values.
860         $extraquestionfields = $this->extra_question_fields();
861         if (is_array($extraquestionfields)) {
862             // Omit table name.
863             array_shift($extraquestionfields);
864             foreach ($extraquestionfields as $field) {
865                 $question->$field = $questiondata->options->$field;
866             }
867         }
869         $this->initialise_question_hints($question, $questiondata);
870     }
872     /**
873      * Initialise question_definition::hints field.
874      * @param question_definition $question the question_definition we are creating.
875      * @param object $questiondata the question data loaded from the database.
876      */
877     protected function initialise_question_hints(question_definition $question, $questiondata) {
878         if (empty($questiondata->hints)) {
879             return;
880         }
881         foreach ($questiondata->hints as $hint) {
882             $question->hints[] = $this->make_hint($hint);
883         }
884     }
886     /**
887      * Create a question_hint, or an appropriate subclass for this question,
888      * from a row loaded from the database.
889      * @param object $hint the DB row from the question hints table.
890      * @return question_hint
891      */
892     protected function make_hint($hint) {
893         return question_hint::load_from_record($hint);
894     }
896     /**
897      * Initialise the combined feedback fields.
898      * @param question_definition $question the question_definition we are creating.
899      * @param object $questiondata the question data loaded from the database.
900      * @param bool $withparts whether to set the shownumcorrect field.
901      */
902     protected function initialise_combined_feedback(question_definition $question,
903             $questiondata, $withparts = false) {
904         $question->correctfeedback = $questiondata->options->correctfeedback;
905         $question->correctfeedbackformat = $questiondata->options->correctfeedbackformat;
906         $question->partiallycorrectfeedback = $questiondata->options->partiallycorrectfeedback;
907         $question->partiallycorrectfeedbackformat =
908                 $questiondata->options->partiallycorrectfeedbackformat;
909         $question->incorrectfeedback = $questiondata->options->incorrectfeedback;
910         $question->incorrectfeedbackformat = $questiondata->options->incorrectfeedbackformat;
911         if ($withparts) {
912             $question->shownumcorrect = $questiondata->options->shownumcorrect;
913         }
914     }
916     /**
917      * Initialise question_definition::answers field.
918      * @param question_definition $question the question_definition we are creating.
919      * @param object $questiondata the question data loaded from the database.
920      * @param bool $forceplaintextanswers most qtypes assume that answers are
921      *      FORMAT_PLAIN, and dont use the answerformat DB column (it contains
922      *      the default 0 = FORMAT_MOODLE). Therefore, by default this method
923      *      ingores answerformat. Pass false here to use answerformat. For example
924      *      multichoice does this.
925      */
926     protected function initialise_question_answers(question_definition $question,
927             $questiondata, $forceplaintextanswers = true) {
928         $question->answers = array();
929         if (empty($questiondata->options->answers)) {
930             return;
931         }
932         foreach ($questiondata->options->answers as $a) {
933             $question->answers[$a->id] = $this->make_answer($a);
934             if (!$forceplaintextanswers) {
935                 $question->answers[$a->id]->answerformat = $a->answerformat;
936             }
937         }
938     }
940     /**
941      * Create a question_answer, or an appropriate subclass for this question,
942      * from a row loaded from the database.
943      * @param object $answer the DB row from the question_answers table plus extra answer fields.
944      * @return question_answer
945      */
946     protected function make_answer($answer) {
947         return new question_answer($answer->id, $answer->answer,
948                     $answer->fraction, $answer->feedback, $answer->feedbackformat);
949     }
951     /**
952      * Deletes the question-type specific data when a question is deleted.
953      * @param int $question the question being deleted.
954      * @param int $contextid the context this quesiotn belongs to.
955      */
956     public function delete_question($questionid, $contextid) {
957         global $DB;
959         $this->delete_files($questionid, $contextid);
961         $extraquestionfields = $this->extra_question_fields();
962         if (is_array($extraquestionfields)) {
963             $question_extension_table = array_shift($extraquestionfields);
964             $DB->delete_records($question_extension_table,
965                     array($this->questionid_column_name() => $questionid));
966         }
968         $extraanswerfields = $this->extra_answer_fields();
969         if (is_array($extraanswerfields)) {
970             $answer_extension_table = array_shift($extraanswerfields);
971             $DB->delete_records_select($answer_extension_table,
972                     'answerid IN (SELECT qa.id FROM {question_answers} qa WHERE qa.question = ?)',
973                     array($questionid));
974         }
976         $DB->delete_records('question_answers', array('question' => $questionid));
978         $DB->delete_records('question_hints', array('questionid' => $questionid));
979     }
981     /**
982      * Returns the number of question numbers which are used by the question
983      *
984      * This function returns the number of question numbers to be assigned
985      * to the question. Most question types will have length one; they will be
986      * assigned one number. The 'description' type, however does not use up a
987      * number and so has a length of zero. Other question types may wish to
988      * handle a bundle of questions and hence return a number greater than one.
989      * @return int         The number of question numbers which should be
990      *                         assigned to the question.
991      * @param object $question The question whose length is to be determined.
992      *                         Question type specific information is included.
993      */
994     public function actual_number_of_questions($question) {
995         // By default, each question is given one number.
996         return 1;
997     }
999     /**
1000      * @param object $question
1001      * @return number|null either a fraction estimating what the student would
1002      * score by guessing, or null, if it is not possible to estimate.
1003      */
1004     public function get_random_guess_score($questiondata) {
1005         return 0;
1006     }
1008     /**
1009      * Whether or not to break down question stats and response analysis, for a question defined by $questiondata.
1010      *
1011      * @param object $questiondata The full question definition data.
1012      * @return bool
1013      */
1014     public function break_down_stats_and_response_analysis_by_variant($questiondata) {
1015         return true;
1016     }
1018     /**
1019      * This method should return all the possible types of response that are
1020      * recognised for this question.
1021      *
1022      * The question is modelled as comprising one or more subparts. For each
1023      * subpart, there are one or more classes that that students response
1024      * might fall into, each of those classes earning a certain score.
1025      *
1026      * For example, in a shortanswer question, there is only one subpart, the
1027      * text entry field. The response the student gave will be classified according
1028      * to which of the possible $question->options->answers it matches.
1029      *
1030      * For the matching question type, there will be one subpart for each
1031      * question stem, and for each stem, each of the possible choices is a class
1032      * of student's response.
1033      *
1034      * A response is an object with two fields, ->responseclass is a string
1035      * presentation of that response, and ->fraction, the credit for a response
1036      * in that class.
1037      *
1038      * Array keys have no specific meaning, but must be unique, and must be
1039      * the same if this function is called repeatedly.
1040      *
1041      * @param object $question the question definition data.
1042      * @return array keys are subquestionid, values are arrays of possible
1043      *      responses to that subquestion.
1044      */
1045     public function get_possible_responses($questiondata) {
1046         return array();
1047     }
1049     /**
1050      * Utility method used by {@link qtype_renderer::head_code()}. It looks
1051      * for any of the files script.js or script.php that exist in the plugin
1052      * folder and ensures they get included.
1053      */
1054     public function find_standard_scripts() {
1055         global $PAGE;
1057         $plugindir = $this->plugin_dir();
1058         $plugindirrel = 'question/type/' . $this->name();
1060         if (file_exists($plugindir . '/script.js')) {
1061             $PAGE->requires->js('/' . $plugindirrel . '/script.js');
1062         }
1063         if (file_exists($plugindir . '/script.php')) {
1064             $PAGE->requires->js('/' . $plugindirrel . '/script.php');
1065         }
1066     }
1068     /**
1069      * Returns true if the editing wizard is finished, false otherwise.
1070      *
1071      * The default implementation returns true, which is suitable for all question-
1072      * types that only use one editing form. This function is used in
1073      * question.php to decide whether we can regrade any states of the edited
1074      * question and redirect to edit.php.
1075      *
1076      * The dataset dependent question-type, which is extended by the calculated
1077      * question-type, overwrites this method because it uses multiple pages (i.e.
1078      * a wizard) to set up the question and associated datasets.
1079      *
1080      * @param object $form  The data submitted by the previous page.
1081      *
1082      * @return bool      Whether the wizard's last page was submitted or not.
1083      */
1084     public function finished_edit_wizard($form) {
1085         // In the default case there is only one edit page.
1086         return true;
1087     }
1089     // IMPORT/EXPORT FUNCTIONS --------------------------------- .
1091     /*
1092      * Imports question from the Moodle XML format
1093      *
1094      * Imports question using information from extra_question_fields function
1095      * If some of you fields contains id's you'll need to reimplement this
1096      */
1097     public function import_from_xml($data, $question, qformat_xml $format, $extra=null) {
1098         $question_type = $data['@']['type'];
1099         if ($question_type != $this->name()) {
1100             return false;
1101         }
1103         $extraquestionfields = $this->extra_question_fields();
1104         if (!is_array($extraquestionfields)) {
1105             return false;
1106         }
1108         // Omit table name.
1109         array_shift($extraquestionfields);
1110         $qo = $format->import_headers($data);
1111         $qo->qtype = $question_type;
1113         foreach ($extraquestionfields as $field) {
1114             $qo->$field = $format->getpath($data, array('#', $field, 0, '#'), '');
1115         }
1117         // Run through the answers.
1118         $answers = $data['#']['answer'];
1119         $a_count = 0;
1120         $extraanswersfields = $this->extra_answer_fields();
1121         if (is_array($extraanswersfields)) {
1122             array_shift($extraanswersfields);
1123         }
1124         foreach ($answers as $answer) {
1125             $ans = $format->import_answer($answer);
1126             if (!$this->has_html_answers()) {
1127                 $qo->answer[$a_count] = $ans->answer['text'];
1128             } else {
1129                 $qo->answer[$a_count] = $ans->answer;
1130             }
1131             $qo->fraction[$a_count] = $ans->fraction;
1132             $qo->feedback[$a_count] = $ans->feedback;
1133             if (is_array($extraanswersfields)) {
1134                 foreach ($extraanswersfields as $field) {
1135                     $qo->{$field}[$a_count] =
1136                         $format->getpath($answer, array('#', $field, 0, '#'), '');
1137                 }
1138             }
1139             ++$a_count;
1140         }
1141         return $qo;
1142     }
1144     /*
1145      * Export question to the Moodle XML format
1146      *
1147      * Export question using information from extra_question_fields function
1148      * If some of you fields contains id's you'll need to reimplement this
1149      */
1150     public function export_to_xml($question, qformat_xml $format, $extra=null) {
1151         $extraquestionfields = $this->extra_question_fields();
1152         if (!is_array($extraquestionfields)) {
1153             return false;
1154         }
1156         // Omit table name.
1157         array_shift($extraquestionfields);
1158         $expout='';
1159         foreach ($extraquestionfields as $field) {
1160             $exportedvalue = $format->xml_escape($question->options->$field);
1161             $expout .= "    <{$field}>{$exportedvalue}</{$field}>\n";
1162         }
1164         $extraanswersfields = $this->extra_answer_fields();
1165         if (is_array($extraanswersfields)) {
1166             array_shift($extraanswersfields);
1167         }
1168         foreach ($question->options->answers as $answer) {
1169             $extra = '';
1170             if (is_array($extraanswersfields)) {
1171                 foreach ($extraanswersfields as $field) {
1172                     $exportedvalue = $format->xml_escape($answer->$field);
1173                     $extra .= "      <{$field}>{$exportedvalue}</{$field}>\n";
1174                 }
1175             }
1177             $expout .= $format->write_answer($answer, $extra);
1178         }
1179         return $expout;
1180     }
1182     /**
1183      * Abstract function implemented by each question type. It runs all the code
1184      * required to set up and save a question of any type for testing purposes.
1185      * Alternate DB table prefix may be used to facilitate data deletion.
1186      */
1187     public function generate_test($name, $courseid=null) {
1188         $form = new stdClass();
1189         $form->name = $name;
1190         $form->questiontextformat = 1;
1191         $form->questiontext = 'test question, generated by script';
1192         $form->defaultmark = 1;
1193         $form->penalty = 0.3333333;
1194         $form->generalfeedback = "Well done";
1196         $context = context_course::instance($courseid);
1197         $newcategory = question_make_default_categories(array($context));
1198         $form->category = $newcategory->id . ',1';
1200         $question = new stdClass();
1201         $question->courseid = $courseid;
1202         $question->qtype = $this->qtype;
1203         return array($form, $question);
1204     }
1206     /**
1207      * Get question context by category id
1208      * @param int $category
1209      * @return object $context
1210      */
1211     protected function get_context_by_category_id($category) {
1212         global $DB;
1213         $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$category));
1214         $context = context::instance_by_id($contextid, IGNORE_MISSING);
1215         return $context;
1216     }
1218     /**
1219      * Save the file belonging to one text field.
1220      *
1221      * @param array $field the data from the form (or from import). This will
1222      *      normally have come from the formslib editor element, so it will be an
1223      *      array with keys 'text', 'format' and 'itemid'. However, when we are
1224      *      importing, it will be an array with keys 'text', 'format' and 'files'
1225      * @param object $context the context the question is in.
1226      * @param string $component indentifies the file area question.
1227      * @param string $filearea indentifies the file area questiontext,
1228      *      generalfeedback, answerfeedback, etc.
1229      * @param int $itemid identifies the file area.
1230      *
1231      * @return string the text for this field, after files have been processed.
1232      */
1233     protected function import_or_save_files($field, $context, $component, $filearea, $itemid) {
1234         if (!empty($field['itemid'])) {
1235             // This is the normal case. We are safing the questions editing form.
1236             return file_save_draft_area_files($field['itemid'], $context->id, $component,
1237                     $filearea, $itemid, $this->fileoptions, trim($field['text']));
1239         } else if (!empty($field['files'])) {
1240             // This is the case when we are doing an import.
1241             foreach ($field['files'] as $file) {
1242                 $this->import_file($context, $component,  $filearea, $itemid, $file);
1243             }
1244         }
1245         return trim($field['text']);
1246     }
1248     /**
1249      * Move all the files belonging to this question from one context to another.
1250      * @param int $questionid the question being moved.
1251      * @param int $oldcontextid the context it is moving from.
1252      * @param int $newcontextid the context it is moving to.
1253      */
1254     public function move_files($questionid, $oldcontextid, $newcontextid) {
1255         $fs = get_file_storage();
1256         $fs->move_area_files_to_new_context($oldcontextid,
1257                 $newcontextid, 'question', 'questiontext', $questionid);
1258         $fs->move_area_files_to_new_context($oldcontextid,
1259                 $newcontextid, 'question', 'generalfeedback', $questionid);
1260     }
1262     /**
1263      * Move all the files belonging to this question's answers when the question
1264      * is moved from one context to another.
1265      * @param int $questionid the question being moved.
1266      * @param int $oldcontextid the context it is moving from.
1267      * @param int $newcontextid the context it is moving to.
1268      * @param bool $answerstoo whether there is an 'answer' question area,
1269      *      as well as an 'answerfeedback' one. Default false.
1270      */
1271     protected function move_files_in_answers($questionid, $oldcontextid,
1272             $newcontextid, $answerstoo = false) {
1273         global $DB;
1274         $fs = get_file_storage();
1276         $answerids = $DB->get_records_menu('question_answers',
1277                 array('question' => $questionid), 'id', 'id,1');
1278         foreach ($answerids as $answerid => $notused) {
1279             if ($answerstoo) {
1280                 $fs->move_area_files_to_new_context($oldcontextid,
1281                         $newcontextid, 'question', 'answer', $answerid);
1282             }
1283             $fs->move_area_files_to_new_context($oldcontextid,
1284                     $newcontextid, 'question', 'answerfeedback', $answerid);
1285         }
1286     }
1288     /**
1289      * Move all the files belonging to this question's hints when the question
1290      * is moved from one context to another.
1291      * @param int $questionid the question being moved.
1292      * @param int $oldcontextid the context it is moving from.
1293      * @param int $newcontextid the context it is moving to.
1294      * @param bool $answerstoo whether there is an 'answer' question area,
1295      *      as well as an 'answerfeedback' one. Default false.
1296      */
1297     protected function move_files_in_hints($questionid, $oldcontextid, $newcontextid) {
1298         global $DB;
1299         $fs = get_file_storage();
1301         $hintids = $DB->get_records_menu('question_hints',
1302                 array('questionid' => $questionid), 'id', 'id,1');
1303         foreach ($hintids as $hintid => $notused) {
1304             $fs->move_area_files_to_new_context($oldcontextid,
1305                     $newcontextid, 'question', 'hint', $hintid);
1306         }
1307     }
1309     /**
1310      * Move all the files belonging to this question's answers when the question
1311      * is moved from one context to another.
1312      * @param int $questionid the question being moved.
1313      * @param int $oldcontextid the context it is moving from.
1314      * @param int $newcontextid the context it is moving to.
1315      * @param bool $answerstoo whether there is an 'answer' question area,
1316      *      as well as an 'answerfeedback' one. Default false.
1317      */
1318     protected function move_files_in_combined_feedback($questionid, $oldcontextid,
1319             $newcontextid) {
1320         global $DB;
1321         $fs = get_file_storage();
1323         $fs->move_area_files_to_new_context($oldcontextid,
1324                 $newcontextid, 'question', 'correctfeedback', $questionid);
1325         $fs->move_area_files_to_new_context($oldcontextid,
1326                 $newcontextid, 'question', 'partiallycorrectfeedback', $questionid);
1327         $fs->move_area_files_to_new_context($oldcontextid,
1328                 $newcontextid, 'question', 'incorrectfeedback', $questionid);
1329     }
1331     /**
1332      * Delete all the files belonging to this question.
1333      * @param int $questionid the question being deleted.
1334      * @param int $contextid the context the question is in.
1335      */
1336     protected function delete_files($questionid, $contextid) {
1337         $fs = get_file_storage();
1338         $fs->delete_area_files($contextid, 'question', 'questiontext', $questionid);
1339         $fs->delete_area_files($contextid, 'question', 'generalfeedback', $questionid);
1340     }
1342     /**
1343      * Delete all the files belonging to this question's answers.
1344      * @param int $questionid the question being deleted.
1345      * @param int $contextid the context the question is in.
1346      * @param bool $answerstoo whether there is an 'answer' question area,
1347      *      as well as an 'answerfeedback' one. Default false.
1348      */
1349     protected function delete_files_in_answers($questionid, $contextid, $answerstoo = false) {
1350         global $DB;
1351         $fs = get_file_storage();
1353         $answerids = $DB->get_records_menu('question_answers',
1354                 array('question' => $questionid), 'id', 'id,1');
1355         foreach ($answerids as $answerid => $notused) {
1356             if ($answerstoo) {
1357                 $fs->delete_area_files($contextid, 'question', 'answer', $answerid);
1358             }
1359             $fs->delete_area_files($contextid, 'question', 'answerfeedback', $answerid);
1360         }
1361     }
1363     /**
1364      * Delete all the files belonging to this question's hints.
1365      * @param int $questionid the question being deleted.
1366      * @param int $contextid the context the question is in.
1367      */
1368     protected function delete_files_in_hints($questionid, $contextid) {
1369         global $DB;
1370         $fs = get_file_storage();
1372         $hintids = $DB->get_records_menu('question_hints',
1373                 array('questionid' => $questionid), 'id', 'id,1');
1374         foreach ($hintids as $hintid => $notused) {
1375             $fs->delete_area_files($contextid, 'question', 'hint', $hintid);
1376         }
1377     }
1379     /**
1380      * Delete all the files belonging to this question's answers.
1381      * @param int $questionid the question being deleted.
1382      * @param int $contextid the context the question is in.
1383      * @param bool $answerstoo whether there is an 'answer' question area,
1384      *      as well as an 'answerfeedback' one. Default false.
1385      */
1386     protected function delete_files_in_combined_feedback($questionid, $contextid) {
1387         global $DB;
1388         $fs = get_file_storage();
1390         $fs->delete_area_files($contextid,
1391                 'question', 'correctfeedback', $questionid);
1392         $fs->delete_area_files($contextid,
1393                 'question', 'partiallycorrectfeedback', $questionid);
1394         $fs->delete_area_files($contextid,
1395                 'question', 'incorrectfeedback', $questionid);
1396     }
1398     public function import_file($context, $component, $filearea, $itemid, $file) {
1399         $fs = get_file_storage();
1400         $record = new stdClass();
1401         if (is_object($context)) {
1402             $record->contextid = $context->id;
1403         } else {
1404             $record->contextid = $context;
1405         }
1406         $record->component = $component;
1407         $record->filearea  = $filearea;
1408         $record->itemid    = $itemid;
1409         $record->filename  = $file->name;
1410         $record->filepath  = '/';
1411         return $fs->create_file_from_string($record, $this->decode_file($file));
1412     }
1414     protected function decode_file($file) {
1415         switch ($file->encoding) {
1416             case 'base64':
1417             default:
1418                 return base64_decode($file->content);
1419         }
1420     }
1424 /**
1425  * This class is used in the return value from
1426  * {@link question_type::get_possible_responses()}.
1427  *
1428  * @copyright  2010 The Open University
1429  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1430  */
1431 class question_possible_response {
1432     /**
1433      * @var string the classification of this response the student gave to this
1434      * part of the question. Must match one of the responseclasses returned by
1435      * {@link question_type::get_possible_responses()}.
1436      */
1437     public $responseclass;
1439     /** @var string the (partial) credit awarded for this responses. */
1440     public $fraction;
1442     /**
1443      * Constructor, just an easy way to set the fields.
1444      * @param string $responseclassid see the field descriptions above.
1445      * @param string $response see the field descriptions above.
1446      * @param number $fraction see the field descriptions above.
1447      */
1448     public function __construct($responseclass, $fraction) {
1449         $this->responseclass = $responseclass;
1450         $this->fraction = $fraction;
1451     }
1453     public static function no_response() {
1454         return new question_possible_response(get_string('noresponse', 'question'), 0);
1455     }