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