74543f1713a010cfad42a47cc4d562af5bd40372
[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  */
30 require_once($CFG->libdir . '/questionlib.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 tyeps 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  * @package questionbank
46  * @subpackage questiontypes
47  */
48 class default_questiontype {
49     public static $fileoptions = array('subdirs'=>false, 'maxfiles'=>-1, 'maxbytes'=>0);
51     /**
52      * Name of the question type
53      *
54      * The name returned should coincide with the name of the directory
55      * in which this questiontype is located
56      *
57      * @return string the name of this question type.
58      */
59     function name() {
60         return 'default';
61     }
63     /**
64      * Returns a list of other question types that this one requires in order to
65      * work. For example, the calculated question type is a subclass of the
66      * numerical question type, which is a subclass of the shortanswer question
67      * type; and the randomsamatch question type requires the shortanswer type
68      * to be installed.
69      *
70      * @return array any other question types that this one relies on. An empty
71      * array if none.
72      */
73     function requires_qtypes() {
74         return array();
75     }
77     /**
78      * @return string the name of this pluginfor passing to get_string, set/get_config, etc.
79      */
80     function plugin_name() {
81         return 'qtype_' . $this->name();
82     }
84     /**
85      * @return string the name of this question type in the user's language.
86      * You should not need to override this method, the default behaviour should be fine.
87      */
88     function local_name() {
89         return get_string($this->name(), $this->plugin_name());
90     }
92     /**
93      * The name this question should appear as in the create new question
94      * dropdown. Override this method to return false if you don't want your
95      * question type to be createable, for example if it is an abstract base type,
96      * otherwise, you should not need to override this method.
97      *
98      * @return mixed the desired string, or false to hide this question type in the menu.
99      */
100     function menu_name() {
101         return $this->local_name();
102     }
104     /**
105      * @return boolean override this to return false if this is not really a
106      *      question type, for example the description question type is not
107      *      really a question type.
108      */
109     function is_real_question_type() {
110         return true;
111     }
113     /**
114      * @return boolean true if this question type may require manual grading.
115      */
116     function is_manual_graded() {
117         return false;
118     }
120     /**
121      * @param object $question a question of this type.
122      * @param string $otherquestionsinuse comma-separate list of other question ids in this attempt.
123      * @return boolean true if a particular instance of this question requires manual grading.
124      */
125     function is_question_manual_graded($question, $otherquestionsinuse) {
126         return $this->is_manual_graded();
127     }
129     /**
130      * @return boolean true if a table analyzing responses should be shown in
131      * the quiz statistics report. Usually if a question is manually graded
132      * then this analysis table won't be a good idea.
133      */
134     function show_analysis_of_responses() {
135         return !$this->is_manual_graded();
136     }
138     /**
139      * @return boolean true if this question type can be used by the random question type.
140      */
141     function is_usable_by_random() {
142         return true;
143     }
145     /**
146      * @param question record.
147      * @param integer subqid this is the id of the subquestion. Usually the id
148      * of the question record of the question record but this is dependent on
149      * the question type. Not relevant to some question types.
150      * @return whether the teacher supplied responses can include wildcards. Can
151      * more than one answer be equivalent to one teacher supplied response.
152      */
153     function has_wildcards_in_responses($question, $subqid) {
154         return false;
155     }
157     /**
158      * @return whether the question_answers.answer field needs to have
159      * restore_decode_content_links_worker called on it.
160      */
161     function has_html_answers() {
162         return false;
163     }
165     /**
166      * If your question type has a table that extends the question table, and
167      * you want the base class to automatically save, backup and restore the extra fields,
168      * override this method to return an array wherer the first element is the table name,
169      * and the subsequent entries are the column names (apart from id and questionid).
170      *
171      * @return mixed array as above, or null to tell the base class to do nothing.
172      */
173     function extra_question_fields() {
174         return null;
175     }
177     /**
178         * If you use extra_question_fields, overload this function to return question id field name
179         *  in case you table use another name for this column
180         */
181     function questionid_column_name() {
182         return 'questionid';
183     }
185     /**
186      * If your question type has a table that extends the question_answers table,
187      * make this method return an array wherer the first element is the table name,
188      * and the subsequent entries are the column names (apart from id and answerid).
189      *
190      * @return mixed array as above, or null to tell the base class to do nothing.
191      */
192     function extra_answer_fields() {
193         return null;
194     }
196     /**
197      * Return an instance of the question editing form definition. This looks for a
198      * class called edit_{$this->name()}_question_form in the file
199      * {$CFG->dirroot}/question/type/{$this->name()}/edit_{$this->name()}_question_form.php
200      * and if it exists returns an instance of it.
201      *
202      * @param string $submiturl passed on to the constructor call.
203      * @return object an instance of the form definition, or null if one could not be found.
204      */
205     function create_editing_form($submiturl, $question, $category, $contexts, $formeditable) {
206         global $CFG;
207         require_once("{$CFG->dirroot}/question/type/edit_question_form.php");
208         $definition_file = $CFG->dirroot.'/question/type/'.$this->name().'/edit_'.$this->name().'_form.php';
209         if (!(is_readable($definition_file) && is_file($definition_file))) {
210             return null;
211         }
212         require_once($definition_file);
213         $classname = 'question_edit_'.$this->name().'_form';
214         if (!class_exists($classname)) {
215             return null;
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     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     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     function display_question_editing_page(&$mform, $question, $wizardnow){
245         global $OUTPUT;
246         $heading = $this->get_heading(empty($question->id));
248         echo $OUTPUT->heading_with_help($heading, $this->name(), $this->plugin_name());
250         $permissionstrs = array();
251         if (!empty($question->id)){
252             if ($question->formoptions->canedit){
253                 $permissionstrs[] = get_string('permissionedit', 'question');
254             }
255             if ($question->formoptions->canmove){
256                 $permissionstrs[] = get_string('permissionmove', 'question');
257             }
258             if ($question->formoptions->cansaveasnew){
259                 $permissionstrs[] = get_string('permissionsaveasnew', 'question');
260             }
261         }
262         if (!$question->formoptions->movecontext  && count($permissionstrs)){
263             echo $OUTPUT->heading(get_string('permissionto', 'question'), 3);
264             $html = '<ul>';
265             foreach ($permissionstrs as $permissionstr){
266                 $html .= '<li>'.$permissionstr.'</li>';
267             }
268             $html .= '</ul>';
269             echo $OUTPUT->box($html, 'boxwidthnarrow boxaligncenter generalbox');
270         }
271         $mform->display();
272     }
274     /**
275      * Method called by display_question_editing_page and by question.php to get heading for breadcrumbs.
276      *
277      * @return array a string heading and the langmodule in which it was found.
278      */
279     function get_heading($adding = false){
280         if ($adding) {
281             $prefix = 'adding';
282         } else {
283             $prefix = 'editing';
284         }
285         return get_string($prefix . $this->name(), $this->plugin_name());
286     }
288     /**
289     * Saves (creates or updates) a question.
290     *
291     * Given some question info and some data about the answers
292     * this function parses, organises and saves the question
293     * It is used by {@link question.php} when saving new data from
294     * a form, and also by {@link import.php} when importing questions
295     * This function in turn calls {@link save_question_options}
296     * to save question-type specific data.
297     *
298     * Whether we are saving a new question or updating an existing one can be
299     * determined by testing !empty($question->id). If it is not empty, we are updating.
300     *
301     * The question will be saved in category $form->category.
302     *
303     * @param object $question the question object which should be updated. For a new question will be mostly empty.
304     * @param object $form the object containing the information to save, as if from the question editing form.
305     * @param object $course not really used any more.
306     * @return object On success, return the new question object. On failure,
307     *       return an object as follows. If the error object has an errors field,
308     *       display that as an error message. Otherwise, the editing form will be
309     *       redisplayed with validation errors, from validation_errors field, which
310     *       is itself an object, shown next to the form fields. (I don't think this is accurate any more.)
311     */
312     function save_question($question, $form, $course) {
313         global $USER, $DB, $OUTPUT;
315         list($question->category) = explode(',', $form->category);
316         $context = $this->get_context_by_category_id($question->category);
318         // This default implementation is suitable for most
319         // question types.
321         // First, save the basic question itself
322         $question->name = trim($form->name);
323         $question->parent = isset($form->parent) ? $form->parent : 0;
324         $question->length = $this->actual_number_of_questions($question);
325         $question->penalty = isset($form->penalty) ? $form->penalty : 0;
327         if (empty($form->questiontext['text'])) {
328             $question->questiontext = '';
329         } else {
330             $question->questiontext = trim($form->questiontext['text']);;
331         }
332         $question->questiontextformat = !empty($form->questiontext['format'])?$form->questiontext['format']:0;
334         if (empty($form->generalfeedback['text'])) {
335             $question->generalfeedback = '';
336         } else {
337             $question->generalfeedback = trim($form->generalfeedback['text']);
338         }
339         $question->generalfeedbackformat = !empty($form->generalfeedback['format'])?$form->generalfeedback['format']:0;
341         if (empty($question->name)) {
342             $question->name = shorten_text(strip_tags($form->questiontext['text']), 15);
343             if (empty($question->name)) {
344                 $question->name = '-';
345             }
346         }
348         if ($question->penalty > 1 or $question->penalty < 0) {
349             $question->errors['penalty'] = get_string('invalidpenalty', 'quiz');
350         }
352         if (isset($form->defaultgrade)) {
353             $question->defaultgrade = $form->defaultgrade;
354         }
356         if (!empty($question->id)) {
357         /// Question already exists, update.
358             $question->modifiedby = $USER->id;
359             $question->timemodified = time();
361             // process queston text
362             $question->questiontext = file_save_draft_area_files($form->questiontext['itemid'], $context->id, 'question', 'questiontext', (int)$question->id, self::$fileoptions, $question->questiontext);
363             // process feedback text
364             $question->generalfeedback = file_save_draft_area_files($form->generalfeedback['itemid'], $context->id, 'question', 'generalfeedback', (int)$question->id, self::$fileoptions, $question->generalfeedback);
365             $DB->update_record('question', $question);
366         } else {
367         /// New question.
368             // Set the unique code
369             $question->stamp = make_unique_id_code();
370             $question->createdby = $USER->id;
371             $question->modifiedby = $USER->id;
372             $question->timecreated = time();
373             $question->timemodified = time();
374             $question->id = $DB->insert_record('question', $question);
375             // process queston text
376             $question->questiontext = file_save_draft_area_files($form->questiontext['itemid'], $context->id, 'question', 'questiontext', (int)$question->id, self::$fileoptions, $question->questiontext);
377             // process feedback text
378             $question->generalfeedback = file_save_draft_area_files($form->generalfeedback['itemid'], $context->id, 'question', 'generalfeedback', (int)$question->id, self::$fileoptions, $question->generalfeedback);
380             $DB->update_record('question', $question);
381         }
383         // Now to save all the answers and type-specific options
384         $form->id = $question->id;
385         $form->qtype = $question->qtype;
386         $form->category = $question->category;
387         $form->questiontext = $question->questiontext;
388         $form->questiontextformat = $question->questiontextformat;
389         // current context
390         $form->context = $context;
392         $result = $this->save_question_options($form);
394         if (!empty($result->error)) {
395             print_error($result->error);
396         }
398         if (!empty($result->notice)) {
399             notice($result->notice, "question.php?id=$question->id");
400         }
402         if (!empty($result->noticeyesno)) {
403             echo $OUTPUT->confirm($result->noticeyesno, "question.php?id=$question->id&courseid={$course->id}",
404                     "edit.php?courseid={$course->id}");
405             echo $OUTPUT->footer();
406             exit;
407         }
409         // Give the question a unique version stamp determined by question_hash()
410         $DB->set_field('question', 'version', question_hash($question), array('id' => $question->id));
412         return $question;
413     }
415     /**
416     * Saves question-type specific options
417     *
418     * This is called by {@link save_question()} to save the question-type specific data
419     * @return object $result->error or $result->noticeyesno or $result->notice
420     * @param object $question  This holds the information from the editing form,
421     *                          it is not a standard question object.
422     */
423     function save_question_options($question) {
424         global $DB;
425         $extra_question_fields = $this->extra_question_fields();
427         if (is_array($extra_question_fields)) {
428             $question_extension_table = array_shift($extra_question_fields);
430             $function = 'update_record';
431             $questionidcolname = $this->questionid_column_name();
432             $options = $DB->get_record($question_extension_table, array($questionidcolname => $question->id));
433             if (!$options) {
434                 $function = 'insert_record';
435                 $options = new stdClass;
436                 $options->$questionidcolname = $question->id;
437             }
438             foreach ($extra_question_fields as $field) {
439                 if (!isset($question->$field)) {
440                     $result = new stdClass;
441                     $result->error = "No data for field $field when saving " .
442                             $this->name() . ' question id ' . $question->id;
443                     return $result;
444                 }
445                 $options->$field = $question->$field;
446             }
448             if (!$DB->{$function}($question_extension_table, $options)) {
449                 $result = new stdClass;
450                 $result->error = 'Could not save question options for ' .
451                         $this->name() . ' question id ' . $question->id;
452                 return $result;
453             }
454         }
456         $extra_answer_fields = $this->extra_answer_fields();
457         // TODO save the answers, with any extra data.
459         return null;
460     }
462     /**
463     * Loads the question type specific options for the question.
464     *
465     * This function loads any question type specific options for the
466     * question from the database into the question object. This information
467     * is placed in the $question->options field. A question type is
468     * free, however, to decide on a internal structure of the options field.
469     * @return bool            Indicates success or failure.
470     * @param object $question The question object for the question. This object
471     *                         should be updated to include the question type
472     *                         specific information (it is passed by reference).
473     */
474     function get_question_options(&$question) {
475         global $CFG, $DB, $OUTPUT;
477         if (!isset($question->options)) {
478             $question->options = new object;
479         }
481         $extra_question_fields = $this->extra_question_fields();
482         if (is_array($extra_question_fields)) {
483             $question_extension_table = array_shift($extra_question_fields);
484             $extra_data = $DB->get_record($question_extension_table, array($this->questionid_column_name() => $question->id), implode(', ', $extra_question_fields));
485             if ($extra_data) {
486                 foreach ($extra_question_fields as $field) {
487                     $question->options->$field = $extra_data->$field;
488                 }
489             } else {
490                 echo $OUTPUT->notification("Failed to load question options from the table $question_extension_table for questionid " .
491                         $question->id);
492                 return false;
493             }
494         }
496         $extra_answer_fields = $this->extra_answer_fields();
497         if (is_array($extra_answer_fields)) {
498             $answer_extension_table = array_shift($extra_answer_fields);
499             $question->options->answers = $DB->get_records_sql("
500                     SELECT qa.*, qax." . implode(', qax.', $extra_answer_fields) . "
501                     FROM {question_answers} qa, {$answer_extension_table} qax
502                     WHERE qa.questionid = ? AND qax.answerid = qa.id", array($question->id));
503             if (!$question->options->answers) {
504                 echo $OUTPUT->notification("Failed to load question answers from the table $answer_extension_table for questionid " .
505                         $question->id);
506                 return false;
507             }
508         } else {
509             // Don't check for success or failure because some question types do not use the answers table.
510             $question->options->answers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC');
511         }
513         return true;
514     }
516     /**
517     * Deletes states from the question-type specific tables
518     *
519     * @param string $stateslist  Comma separated list of state ids to be deleted
520     */
521     function delete_states($stateslist) {
522         /// The default question type does not have any tables of its own
523         // therefore there is nothing to delete
525         return true;
526     }
528     /**
529     * Deletes a question from the question-type specific tables
530     *
531     * @return boolean Success/Failure
532     * @param object $question  The question being deleted
533     */
534     function delete_question($questionid) {
535         global $CFG, $DB;
536         $success = true;
538         $extra_question_fields = $this->extra_question_fields();
539         if (is_array($extra_question_fields)) {
540             $question_extension_table = array_shift($extra_question_fields);
541             $success = $success && $DB->delete_records($question_extension_table,
542                     array($this->questionid_column_name() => $questionid));
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             $success = $success && $DB->delete_records_select($answer_extension_table,
549                 "answerid IN (SELECT qa.id FROM {question_answers} qa WHERE qa.question = ?)", array($questionid));
550         }
552         $success = $success && $DB->delete_records('question_answers', array('question' => $questionid));
554         return $success;
555     }
557     /**
558     * Returns the number of question numbers which are used by the question
559     *
560     * This function returns the number of question numbers to be assigned
561     * to the question. Most question types will have length one; they will be
562     * assigned one number. The 'description' type, however does not use up a
563     * number and so has a length of zero. Other question types may wish to
564     * handle a bundle of questions and hence return a number greater than one.
565     * @return integer         The number of question numbers which should be
566     *                         assigned to the question.
567     * @param object $question The question whose length is to be determined.
568     *                         Question type specific information is included.
569     */
570     function actual_number_of_questions($question) {
571         // By default, each question is given one number
572         return 1;
573     }
575     /**
576     * Creates empty session and response information for the question
577     *
578     * This function is called to start a question session. Empty question type
579     * specific session data (if any) and empty response data will be added to the
580     * state object. Session data is any data which must persist throughout the
581     * attempt possibly with updates as the user interacts with the
582     * question. This function does NOT create new entries in the database for
583     * the session; a call to the {@link save_session_and_responses} member will
584     * occur to do this.
585     * @return bool            Indicates success or failure.
586     * @param object $question The question for which the session is to be
587     *                         created. Question type specific information is
588     *                         included.
589     * @param object $state    The state to create the session for. Note that
590     *                         this will not have been saved in the database so
591     *                         there will be no id. This object will be updated
592     *                         to include the question type specific information
593     *                         (it is passed by reference). In particular, empty
594     *                         responses will be created in the ->responses
595     *                         field.
596     * @param object $cmoptions
597     * @param object $attempt  The attempt for which the session is to be
598     *                         started. Questions may wish to initialize the
599     *                         session in different ways depending on the user id
600     *                         or time available for the attempt.
601     */
602     function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
603         // The default implementation should work for the legacy question types.
604         // Most question types with only a single form field for the student's response
605         // will use the empty string '' as the index for that one response. This will
606         // automatically be stored in and restored from the answer field in the
607         // question_states table.
608         $state->responses = array(
609                 '' => '',
610         );
611         return true;
612     }
614     /**
615     * Restores the session data and most recent responses for the given state
616     *
617     * This function loads any session data associated with the question
618     * session in the given state from the database into the state object.
619     * In particular it loads the responses that have been saved for the given
620     * state into the ->responses member of the state object.
621     *
622     * Question types with only a single form field for the student's response
623     * will not need not restore the responses; the value of the answer
624     * field in the question_states table is restored to ->responses['']
625     * before this function is called. Question types with more response fields
626     * should override this method and set the ->responses field to an
627     * associative array of responses.
628     * @return bool            Indicates success or failure.
629     * @param object $question The question object for the question including any
630     *                         question type specific information.
631     * @param object $state    The saved state to load the session for. This
632     *                         object should be updated to include the question
633     *                         type specific session information and responses
634     *                         (it is passed by reference).
635     */
636     function restore_session_and_responses(&$question, &$state) {
637         // The default implementation does nothing (successfully)
638         return true;
639     }
641     /**
642     * Saves the session data and responses for the given question and state
643     *
644     * This function saves the question type specific session data from the
645     * state object to the database. In particular for most question types it saves the
646     * responses from the ->responses member of the state object. The question type
647     * non-specific data for the state has already been saved in the question_states
648     * table and the state object contains the corresponding id and
649     * sequence number which may be used to index a question type specific table.
650     *
651     * Question types with only a single form field for the student's response
652     * which is contained in ->responses[''] will not have to save this response,
653     * it will already have been saved to the answer field of the question_states table.
654     * Question types with more response fields should override this method to convert
655     * the data the ->responses array into a single string field, and save it in the
656     * database. The implementation in the multichoice question type is a good model to follow.
657     * http://cvs.moodle.org/contrib/plugins/question/type/opaque/questiontype.php?view=markup
658     * has a solution that is probably quite generally applicable.
659     * @return bool            Indicates success or failure.
660     * @param object $question The question object for the question including
661     *                         the question type specific information.
662     * @param object $state    The state for which the question type specific
663     *                         data and responses should be saved.
664     */
665     function save_session_and_responses(&$question, &$state) {
666         // The default implementation does nothing (successfully)
667         return true;
668     }
670     /**
671     * Returns an array of values which will give full marks if graded as
672     * the $state->responses field
673     *
674     * The correct answer to the question in the given state, or an example of
675     * a correct answer if there are many, is returned. This is used by some question
676     * types in the {@link grade_responses()} function but it is also used by the
677     * question preview screen to fill in correct responses.
678     * @return mixed           A response array giving the responses corresponding
679     *                         to the (or a) correct answer to the question. If there is
680     *                         no correct answer that scores 100% then null is returned.
681     * @param object $question The question for which the correct answer is to
682     *                         be retrieved. Question type specific information is
683     *                         available.
684     * @param object $state    The state of the question, for which a correct answer is
685     *                         needed. Question type specific information is included.
686     */
687     function get_correct_responses(&$question, &$state) {
688         /* The default implementation returns the response for the first answer
689         that gives full marks. */
690         if ($question->options->answers) {
691             foreach ($question->options->answers as $answer) {
692                 if (((int) $answer->fraction) === 1) {
693                     return array('' => $answer->answer);
694                 }
695             }
696         }
697         return null;
698     }
700     /**
701     * Return an array of values with the texts for all possible responses stored
702     * for the question
703     *
704     * All answers are found and their text values isolated
705     * @return object          A mixed object
706     *             ->id        question id. Needed to manage random questions:
707     *                         it's the id of the actual question presented to user in a given attempt
708     *             ->responses An array of values giving the responses corresponding
709     *                         to all answers to the question. Answer ids are used as keys.
710     *                         The text and partial credit are the object components
711     * @param object $question The question for which the answers are to
712     *                         be retrieved. Question type specific information is
713     *                         available.
714     */
715     // ULPGC ecastro
716     function get_all_responses(&$question, &$state) {
717         if (isset($question->options->answers) && is_array($question->options->answers)) {
718             $answers = array();
719             foreach ($question->options->answers as $aid=>$answer) {
720                 $r = new stdClass;
721                 $r->answer = $answer->answer;
722                 $r->credit = $answer->fraction;
723                 $answers[$aid] = $r;
724             }
725             $result = new stdClass;
726             $result->id = $question->id;
727             $result->responses = $answers;
728             return $result;
729         } else {
730             return null;
731         }
732     }
733     /**
734      * The difference between this method an get_all_responses is that this
735      * method is not passed a state object. It is the possible answers to a
736      * question no matter what the state.
737      * This method is not called for random questions.
738      * @return array of possible answers.
739      */
740     function get_possible_responses(&$question) {
741         static $responses = array();
742         if (!isset($responses[$question->id])){
743             $responses[$question->id] = $this->get_all_responses($question, new object());
744         }
745         return array($question->id => $responses[$question->id]->responses);
746     }
748     /**
749      * @param object $question
750      * @return mixed either a integer score out of 1 that the average random
751      * guess by a student might give or an empty string which means will not
752      * calculate.
753      */
754     function get_random_guess_score($question) {
755         return 0;
756     }
757    /**
758     * Return the actual response to the question in a given state
759     * for the question. Text is not yet formatted for output.
760     *
761     * @return mixed           An array containing the response or reponses (multiple answer, match)
762     *                         given by the user in a particular attempt.
763     * @param object $question The question for which the correct answer is to
764     *                         be retrieved. Question type specific information is
765     *                         available.
766     * @param object $state    The state object that corresponds to the question,
767     *                         for which a correct answer is needed. Question
768     *                         type specific information is included.
769     */
770     // ULPGC ecastro
771     function get_actual_response($question, $state) {
772        if (!empty($state->responses)) {
773            $responses[] = $state->responses[''];
774        } else {
775            $responses[] = '';
776        }
777        return $responses;
778     }
780     function get_actual_response_details($question, $state) {
781         $response = array_shift($this->get_actual_response($question, $state));
782         $teacherresponses = $this->get_possible_responses($question, $state);
783         //only one response
784         list($tsubqid, $tresponses) = each($teacherresponses);
785         $responsedetail = new object();
786         $responsedetail->subqid = $tsubqid;
787         $responsedetail->response = $response;
788         if ($aid = $this->check_response($question, $state)){
789             $responsedetail->aid = $aid;
790         } else {
791             foreach ($tresponses as $aid => $tresponse){
792                 if ($tresponse->answer == $response){
793                     $responsedetail->aid = $aid;
794                     break;
795                 }
796             }
797         }
798         if (isset($responsedetail->aid)){
799             $responsedetail->credit = $tresponses[$aid]->credit;
800         } else {
801             $responsedetail->aid = 0;
802             $responsedetail->credit = 0;
803         }
804         return array($responsedetail);
805     }
807     // ULPGC ecastro
808     function get_fractional_grade(&$question, &$state) {
809         $grade = $state->grade;
810         if ($question->maxgrade > 0) {
811             return (float)($grade / $question->maxgrade);
812         } else {
813             return (float)$grade;
814         }
815     }
818     /**
819     * Checks if the response given is correct and returns the id
820     *
821     * @return int             The ide number for the stored answer that matches the response
822     *                         given by the user in a particular attempt.
823     * @param object $question The question for which the correct answer is to
824     *                         be retrieved. Question type specific information is
825     *                         available.
826     * @param object $state    The state object that corresponds to the question,
827     *                         for which a correct answer is needed. Question
828     *                         type specific information is included.
829     */
830     // ULPGC ecastro
831     function check_response(&$question, &$state){
832         return false;
833     }
835     // Used by the following function, so that it only returns results once per quiz page.
836     private $htmlheadalreadydone = false;
837     /**
838      * Hook to allow question types to include required JavaScrip or CSS on pages
839      * where they are going to be printed.
840      *
841      * If this question type requires JavaScript to function,
842      * then this method, which will be called before print_header on any page
843      * where this question is going to be printed, is a chance to call
844      * $PAGE->requires->js, and so on.
845      *
846      * The two parameters match the first two parameters of print_question.
847      *
848      * @param object $question The question object.
849      * @param object $state    The state object.
850      */
851     function get_html_head_contributions(&$question, &$state) {
852         // We only do this once for this question type, no matter how often this
853         // method is called on one page.
854         if ($this->htmlheadalreadydone) {
855             return;
856         }
857         $this->htmlheadalreadydone = true;
859         // By default, we link to any of the files script.js or script.php that
860         // exist in the plugin folder.
861         $this->find_standard_scripts();
862     }
864     /**
865      * Like @see{get_html_head_contributions}, but this method is for CSS and
866      * JavaScript required on the question editing page question/question.php.
867      */
868     function get_editing_head_contributions() {
869         // By default, we link to any of the files styles.css, styles.php,
870         // script.js or script.php that exist in the plugin folder.
871         // Core question types should not use this mechanism. Their styles
872         // should be included in the standard theme.
873         $this->find_standard_scripts();
874     }
876     /**
877      * Utility method used by @see{get_html_head_contributions} and
878      * @see{get_editing_head_contributions}. This looks for any of the files
879      * script.js or script.php that exist in the plugin folder and ensures they
880      * get included.
881      */
882     protected function find_standard_scripts() {
883         global $PAGE;
885         $plugindir = $this->plugin_dir();
886         $plugindirrel = 'question/type/' . $this->name();
888         if (file_exists($plugindir . '/script.js')) {
889             $PAGE->requires->js('/' . $plugindirrel . '/script.js');
890         }
891         if (file_exists($plugindir . '/script.php')) {
892             $PAGE->requires->js('/' . $plugindirrel . '/script.php');
893         }
894     }
896     /**
897      * Prints the question including the number, grading details, content,
898      * feedback and interactions
899      *
900      * This function prints the question including the question number,
901      * grading details, content for the question, any feedback for the previously
902      * submitted responses and the interactions. The default implementation calls
903      * various other methods to print each of these parts and most question types
904      * will just override those methods.
905      * @param object $question The question to be rendered. Question type
906      *                         specific information is included. The
907      *                         maximum possible grade is in ->maxgrade. The name
908      *                         prefix for any named elements is in ->name_prefix.
909      * @param object $state    The state to render the question in. The grading
910      *                         information is in ->grade, ->raw_grade and
911      *                         ->penalty. The current responses are in
912      *                         ->responses. This is an associative array (or the
913      *                         empty string or null in the case of no responses
914      *                         submitted). The last graded state is in
915      *                         ->last_graded (hence the most recently graded
916      *                         responses are in ->last_graded->responses). The
917      *                         question type specific information is also
918      *                         included.
919      * @param integer $number  The number for this question.
920      * @param object $cmoptions
921      * @param object $options  An object describing the rendering options.
922      */
923     function print_question(&$question, &$state, $number, $cmoptions, $options, $context=null) {
924         /* The default implementation should work for most question types
925         provided the member functions it calls are overridden where required.
926         The layout is determined by the template question.html */
928         global $CFG, $OUTPUT;
930         $context = $this->get_context_by_category_id($question->category);
931         $question->questiontext = quiz_rewrite_question_urls($question->questiontext, 'pluginfile.php', $context->id, 'question', 'questiontext', array($state->attempt, $state->question), $question->id);
933         $question->generalfeedback = quiz_rewrite_question_urls($question->generalfeedback, 'pluginfile.php', $context->id, 'question', 'generalfeedback', array($state->attempt, $state->question), $question->id);
935         $isgraded = question_state_is_graded($state->last_graded);
937         if (isset($question->randomquestionid)) {
938             $actualquestionid = $question->randomquestionid;
939         } else {
940             $actualquestionid = $question->id;
941         }
943         // For editing teachers print a link to an editing popup window
944         $editlink = $this->get_question_edit_link($question, $cmoptions, $options);
946         $generalfeedback = '';
947         if ($isgraded && $options->generalfeedback) {
948             $generalfeedback = $this->format_text($question->generalfeedback,
949                     $question->generalfeedbackformat, $cmoptions);
950         }
952         $grade = '';
953         if ($question->maxgrade > 0 && $options->scores) {
954             if ($cmoptions->optionflags & QUESTION_ADAPTIVE) {
955                 if ($isgraded) {
956                     $grade = question_format_grade($cmoptions, $state->last_graded->grade).'/';
957                 } else {
958                     $grade = '--/';
959                 }
960             }
961             $grade .= question_format_grade($cmoptions, $question->maxgrade);
962         }
964         $formatoptions = new stdClass;
965         $formatoptions->para = false;
966         $comment = format_text($state->manualcomment, FORMAT_HTML,
967                 $formatoptions, $cmoptions->course);
968         $commentlink = '';
970         if (!empty($options->questioncommentlink)) {
971             $strcomment = get_string('commentorgrade', 'quiz');
973             $link = new moodle_url("$options->questioncommentlink?attempt=$state->attempt&question=$actualquestionid");
974             $action = new popup_action('click', $link, 'commentquestion', array('height' => 480, 'width' => 750));
975             $commentlink = $OUTPUT->container($OUTPUT->action_link($link, $strcomment, $action), 'commentlink');
976         }
978         $history = $this->history($question, $state, $number, $cmoptions, $options);
980         include "$CFG->dirroot/question/type/question.html";
981     }
983     /**
984      * Render the question flag, assuming $flagsoption allows it. You will probably
985      * never need to override this method.
986      *
987      * @param object $question the question
988      * @param object $state its current state
989      * @param integer $flagsoption the option that says whether flags should be displayed.
990      */
991     protected function print_question_flag($question, $state, $flagsoption) {
992         global $CFG, $PAGE;
993         switch ($flagsoption) {
994             case QUESTION_FLAGSSHOWN:
995                 $flagcontent = $this->get_question_flag_tag($state->flagged);
996                 break;
997             case QUESTION_FLAGSEDITABLE:
998                 $id = $question->name_prefix . '_flagged';
999                 if ($state->flagged) {
1000                     $checked = 'checked="checked" ';
1001                 } else {
1002                     $checked = '';
1003                 }
1004                 $qsid = $state->questionsessionid;
1005                 $aid = $state->attempt;
1006                 $qid = $state->question;
1007                 $checksum = question_get_toggleflag_checksum($aid, $qid, $qsid);
1008                 $postdata = "qsid=$qsid&aid=$aid&qid=$qid&checksum=$checksum&sesskey=" .
1009                         sesskey() . '&newstate=';
1010                 $flagcontent = '<input type="checkbox" id="' . $id . '" name="' . $id .
1011                         '" class="questionflagcheckbox" value="1" ' . $checked . ' />' .
1012                         '<input type="hidden" value="' . s($postdata) . '" class="questionflagpostdata" />' .
1013                         '<label id="' . $id . 'label" for="' . $id .
1014                         '" class="questionflaglabel">' . $this->get_question_flag_tag(
1015                         $state->flagged, $id . 'img') . '</label>' . "\n";
1016                 question_init_qengine_js();
1017                 break;
1018             default:
1019                 $flagcontent = '';
1020         }
1021         if ($flagcontent) {
1022             echo '<div class="questionflag">' . $flagcontent . "</div>\n";
1023         }
1024     }
1026     /**
1027      * Work out the actual img tag needed for the flag
1028      *
1029      * @param boolean $flagged whether the question is currently flagged.
1030      * @param string $id an id to be added as an attribute to the img (optional).
1031      * @return string the img tag.
1032      */
1033     protected function get_question_flag_tag($flagged, $id = '') {
1034         global $OUTPUT;
1035         if ($id) {
1036             $id = 'id="' . $id . '" ';
1037         }
1038         if ($flagged) {
1039             $img = 'i/flagged';
1040         } else {
1041             $img = 'i/unflagged';
1042         }
1043         return '<img ' . $id . 'src="' . $OUTPUT->pix_url($img) .
1044                 '" alt="' . get_string('flagthisquestion', 'question') . '" />';
1045     }
1047     /**
1048      * Get a link to an edit icon for this question, if the current user is allowed
1049      * to edit it.
1050      *
1051      * @param object $question the question object.
1052      * @param object $cmoptions the options from the module. If $cmoptions->thispageurl is set
1053      *      then the link will be to edit the question in this browser window, then return to
1054      *      $cmoptions->thispageurl. Otherwise the link will be to edit in a popup.
1055      * @return string the HTML of the link, or nothing it the currenty user is not allowed to edit.
1056      */
1057     function get_question_edit_link($question, $cmoptions, $options) {
1058         global $CFG, $OUTPUT;
1060     /// Is this user allowed to edit this question?
1061         if (!empty($options->noeditlink) || !question_has_capability_on($question, 'edit')) {
1062             return '';
1063         }
1065     /// Work out the right URL.
1066         $linkurl = '/question/question.php?id=' . $question->id;
1067         if (!empty($cmoptions->cmid)) {
1068             $linkurl .= '&amp;cmid=' . $cmoptions->cmid;
1069         } else if (!empty($cmoptions->course)) {
1070             $linkurl .= '&amp;courseid=' . $cmoptions->course;
1071         } else {
1072             print_error('missingcourseorcmidtolink', 'question');
1073         }
1075     /// Work out the contents of the link.
1076         $stredit = get_string('edit');
1077         $linktext = '<img src="' . $OUTPUT->pix_url('t/edit') . '" alt="' . $stredit . '" />';
1079         if (!empty($cmoptions->thispageurl)) {
1080         /// The module allow editing in the same window, print an ordinary link.
1081             return '<a href="' . $CFG->wwwroot . $linkurl . '&amp;returnurl=' .
1082                     urlencode($cmoptions->thispageurl . '#q' . $question->id) .
1083                     '" title="' . $stredit . '">' . $linktext . '</a>';
1084         } else {
1085         /// We have to edit in a pop-up.
1086             $link = new moodle_url($linkurl . '&inpopup=1');
1087             $action = new popup_action('click', $link, 'editquestion');
1088             return $OUTPUT->action_link($link, $linktext, $action ,array('title'=>$stredit));
1089         }
1090     }
1092     /**
1093      * Print history of responses
1094      *
1095      * Used by print_question()
1096      */
1097     function history($question, $state, $number, $cmoptions, $options) {
1098         global $DB, $OUTPUT;
1100         if (empty($options->history)) {
1101             return '';
1102         }
1104         $params = array('aid' => $state->attempt);
1105         if (isset($question->randomquestionid)) {
1106             $params['qid'] = $question->randomquestionid;
1107             $randomprefix = 'random' . $question->id . '-';
1108         } else {
1109             $params['qid'] = $question->id;
1110             $randomprefix = '';
1111         }
1112         if ($options->history == 'all') {
1113             $eventtest = 'event > 0';
1114         } else {
1115             $eventtest = 'event IN (' . QUESTION_EVENTS_GRADED . ')';
1116         }
1117         $states = $DB->get_records_select('question_states',
1118                 'attempt = :aid AND question = :qid AND ' . $eventtest, $params, 'seq_number ASC');
1119         if (count($states) <= 1) {
1120             return '';
1121         }
1123         $strreviewquestion = get_string('reviewresponse', 'quiz');
1124         $table = new html_table();
1125         $table->width = '100%';
1126         $table->head  = array (
1127             get_string('numberabbr', 'quiz'),
1128             get_string('action', 'quiz'),
1129             get_string('response', 'quiz'),
1130             get_string('time'),
1131         );
1132         if ($options->scores) {
1133             $table->head[] = get_string('score', 'quiz');
1134             $table->head[] = get_string('grade', 'quiz');
1135         }
1137         foreach ($states as $st) {
1138             if ($randomprefix && strpos($st->answer, $randomprefix) === 0) {
1139                 $st->answer = substr($st->answer, strlen($randomprefix));
1140             }
1141             $st->responses[''] = $st->answer;
1142             $this->restore_session_and_responses($question, $st);
1144             if ($state->id == $st->id) {
1145                 $link = '<b>' . $st->seq_number . '</b>';
1146             } else if (isset($options->questionreviewlink)) {
1147                 $reviewlink = new moodle_url($options->questionreviewlink);
1148                 $reviewlink->params(array('state'=>$st->id,'question'=>$question->id));
1149                 $link = new moodle_url($reviewlink);
1150                 $action = new popup_action('click', $link, 'reviewquestion', array('height' => 450, 'width' => 650));
1151                 $link = $OUTPUT->action_link($link, $st->seq_number, $action, array('title'=>$strreviewquestion));
1152             } else {
1153                 $link = $st->seq_number;
1154             }
1156             if ($state->id == $st->id) {
1157                 $b = '<b>';
1158                 $be = '</b>';
1159             } else {
1160                 $b = '';
1161                 $be = '';
1162             }
1164             $data = array (
1165                 $link,
1166                 $b.get_string('event'.$st->event, 'quiz').$be,
1167                 $b.$this->response_summary($question, $st).$be,
1168                 $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be,
1169             );
1170             if ($options->scores) {
1171                 $data[] = $b.question_format_grade($cmoptions, $st->raw_grade).$be;
1172                 $data[] = $b.question_format_grade($cmoptions, $st->raw_grade).$be;
1173             }
1174             $table->data[] = $data;
1175         }
1176         return html_writer::table($table);
1177     }
1179     /**
1180     * Prints the score obtained and maximum score available plus any penalty
1181     * information
1182     *
1183     * This function prints a summary of the scoring in the most recently
1184     * graded state (the question may not have been submitted for marking at
1185     * the current state). The default implementation should be suitable for most
1186     * question types.
1187     * @param object $question The question for which the grading details are
1188     *                         to be rendered. Question type specific information
1189     *                         is included. The maximum possible grade is in
1190     *                         ->maxgrade.
1191     * @param object $state    The state. In particular the grading information
1192     *                          is in ->grade, ->raw_grade and ->penalty.
1193     * @param object $cmoptions
1194     * @param object $options  An object describing the rendering options.
1195     */
1196     function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
1197         /* The default implementation prints the number of marks if no attempt
1198         has been made. Otherwise it displays the grade obtained out of the
1199         maximum grade available and a warning if a penalty was applied for the
1200         attempt and displays the overall grade obtained counting all previous
1201         responses (and penalties) */
1203         if (QUESTION_EVENTDUPLICATE == $state->event) {
1204             echo ' ';
1205             print_string('duplicateresponse', 'quiz');
1206         }
1207         if ($question->maxgrade > 0 && $options->scores) {
1208             if (question_state_is_graded($state->last_graded)) {
1209                 // Display the grading details from the last graded state
1210                 $grade = new stdClass;
1211                 $grade->cur = question_format_grade($cmoptions, $state->last_graded->grade);
1212                 $grade->max = question_format_grade($cmoptions, $question->maxgrade);
1213                 $grade->raw = question_format_grade($cmoptions, $state->last_graded->raw_grade);
1215                 // let student know wether the answer was correct
1216                 $class = question_get_feedback_class($state->last_graded->raw_grade /
1217                         $question->maxgrade);
1218                 echo '<div class="correctness ' . $class . '">' . get_string($class, 'quiz') . '</div>';
1220                 echo '<div class="gradingdetails">';
1221                 // print grade for this submission
1222                 print_string('gradingdetails', 'quiz', $grade);
1223                 if ($cmoptions->penaltyscheme) {
1224                     // print details of grade adjustment due to penalties
1225                     if ($state->last_graded->raw_grade > $state->last_graded->grade){
1226                         echo ' ';
1227                         print_string('gradingdetailsadjustment', 'quiz', $grade);
1228                     }
1229                     // print info about new penalty
1230                     // penalty is relevant only if the answer is not correct and further attempts are possible
1231                     if (($state->last_graded->raw_grade < $question->maxgrade / 1.01)
1232                                 and (QUESTION_EVENTCLOSEANDGRADE != $state->event)) {
1234                         if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) {
1235                             // A penalty was applied so display it
1236                             echo ' ';
1237                             print_string('gradingdetailspenalty', 'quiz', question_format_grade($cmoptions, $state->last_graded->penalty));
1238                         } else {
1239                             /* No penalty was applied even though the answer was
1240                             not correct (eg. a syntax error) so tell the student
1241                             that they were not penalised for the attempt */
1242                             echo ' ';
1243                             print_string('gradingdetailszeropenalty', 'quiz');
1244                         }
1245                     }
1246                 }
1247                 echo '</div>';
1248             }
1249         }
1250     }
1252     /**
1253     * Prints the main content of the question including any interactions
1254     *
1255     * This function prints the main content of the question including the
1256     * interactions for the question in the state given. The last graded responses
1257     * are printed or indicated and the current responses are selected or filled in.
1258     * Any names (eg. for any form elements) are prefixed with $question->name_prefix.
1259     * This method is called from the print_question method.
1260     * @param object $question The question to be rendered. Question type
1261     *                         specific information is included. The name
1262     *                         prefix for any named elements is in ->name_prefix.
1263     * @param object $state    The state to render the question in. The grading
1264     *                         information is in ->grade, ->raw_grade and
1265     *                         ->penalty. The current responses are in
1266     *                         ->responses. This is an associative array (or the
1267     *                         empty string or null in the case of no responses
1268     *                         submitted). The last graded state is in
1269     *                         ->last_graded (hence the most recently graded
1270     *                         responses are in ->last_graded->responses). The
1271     *                         question type specific information is also
1272     *                         included.
1273     *                         The state is passed by reference because some adaptive
1274     *                         questions may want to update it during rendering
1275     * @param object $cmoptions
1276     * @param object $options  An object describing the rendering options.
1277     */
1278     function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
1279         /* This default implementation prints an error and must be overridden
1280         by all question type implementations, unless the default implementation
1281         of print_question has been overridden. */
1282         global $OUTPUT;
1283         echo $OUTPUT->notification('Error: Question formulation and input controls has not'
1284                .'  been implemented for question type '.$this->name());
1285     }
1287     function check_file_access($question, $state, $options, $contextid, $component,
1288             $filearea, $args) {
1290         if ($component == 'question' && $filearea == 'questiontext') {
1291             // Question text always visible.
1292             return true;
1294         } else if ($component == 'question' && $filearea = 'generalfeedback') {
1295             return $options->generalfeedback && question_state_is_graded($state->last_graded);
1297         } else {
1298             // Unrecognised component or filearea.
1299             return false;
1300         }
1301     }
1303     /**
1304     * Prints the submit button(s) for the question in the given state
1305     *
1306     * This function prints the submit button(s) for the question in the
1307     * given state. The name of any button created will be prefixed with the
1308     * unique prefix for the question in $question->name_prefix. The suffix
1309     * 'submit' is reserved for the single question submit button and the suffix
1310     * 'validate' is reserved for the single question validate button (for
1311     * question types which support it). Other suffixes will result in a response
1312     * of that name in $state->responses which the printing and grading methods
1313     * can then use.
1314     * @param object $question The question for which the submit button(s) are to
1315     *                         be rendered. Question type specific information is
1316     *                         included. The name prefix for any
1317     *                         named elements is in ->name_prefix.
1318     * @param object $state    The state to render the buttons for. The
1319     *                         question type specific information is also
1320     *                         included.
1321     * @param object $cmoptions
1322     * @param object $options  An object describing the rendering options.
1323     */
1324     function print_question_submit_buttons(&$question, &$state, $cmoptions, $options) {
1325         // The default implementation should be suitable for most question types.
1326         // It prints a mark button in the case where individual marking is allowed.
1327         if (($cmoptions->optionflags & QUESTION_ADAPTIVE) and !$options->readonly) {
1328             echo '<input type="submit" name="', $question->name_prefix, 'submit" value="',
1329                     get_string('mark', 'quiz'), '" class="submit btn" />';
1330         }
1331     }
1333     /**
1334     * Return a summary of the student response
1335     *
1336     * This function returns a short string of no more than a given length that
1337     * summarizes the student's response in the given $state. This is used for
1338     * example in the response history table. This string should already be
1339     * formatted for output.
1340     * @return string         The summary of the student response
1341     * @param object $question
1342     * @param object $state   The state whose responses are to be summarized
1343     * @param int $length     The maximum length of the returned string
1344     */
1345     function response_summary($question, $state, $length = 80, $formatting = true) {
1346         // This should almost certainly be overridden
1347         $responses = $this->get_actual_response($question, $state);
1348         if ($formatting){
1349             $responses = $this->format_responses($responses, $question->questiontextformat);
1350         }
1351         $responses = implode('; ', $responses);
1352         return shorten_text($responses, $length);
1353     }
1354     /**
1355      * @param array responses is an array of responses.
1356      * @return formatted responses
1357      */
1358     function format_responses($responses, $format){
1359         $toreturn = array();
1360         foreach ($responses as $response){
1361             $toreturn[] = $this->format_response($response, $format);
1362         }
1363         return $toreturn;
1364     }
1365     /**
1366      * @param string response is a response.
1367      * @return formatted response
1368      */
1369     function format_response($response, $format){
1370         return s($response);
1371     }
1372     /**
1373     * Renders the question for printing and returns the LaTeX source produced
1374     *
1375     * This function should render the question suitable for a printed problem
1376     * or solution sheet in LaTeX and return the rendered output.
1377     * @return string          The LaTeX output.
1378     * @param object $question The question to be rendered. Question type
1379     *                         specific information is included.
1380     * @param object $state    The state to render the question in. The
1381     *                         question type specific information is also
1382     *                         included.
1383     * @param object $cmoptions
1384     * @param string $type     Indicates if the question or the solution is to be
1385     *                         rendered with the values 'question' and
1386     *                         'solution'.
1387     */
1388     function get_texsource(&$question, &$state, $cmoptions, $type) {
1389         // The default implementation simply returns a string stating that
1390         // the question is only available online.
1392         return get_string('onlineonly', 'texsheet');
1393     }
1395     /**
1396     * Compares two question states for equivalence of the student's responses
1397     *
1398     * The responses for the two states must be examined to see if they represent
1399     * equivalent answers to the question by the student. This method will be
1400     * invoked for each of the previous states of the question before grading
1401     * occurs. If the student is found to have already attempted the question
1402     * with equivalent responses then the attempt at the question is ignored;
1403     * grading does not occur and the state does not change. Thus they are not
1404     * penalized for this case.
1405     * @return boolean
1406     * @param object $question  The question for which the states are to be
1407     *                          compared. Question type specific information is
1408     *                          included.
1409     * @param object $state     The state of the question. The responses are in
1410     *                          ->responses. This is the only field of $state
1411     *                          that it is safe to use.
1412     * @param object $teststate The state whose responses are to be
1413     *                          compared. The state will be of the same age or
1414     *                          older than $state. If possible, the method should
1415     *                          only use the field $teststate->responses, however
1416     *                          any field that is set up by restore_session_and_responses
1417     *                          can be used.
1418     */
1419     function compare_responses(&$question, $state, $teststate) {
1420         // The default implementation performs a comparison of the response
1421         // arrays. The ordering of the arrays does not matter.
1422         // Question types may wish to override this (eg. to ignore trailing
1423         // white space or to make "7.0" and "7" compare equal).
1425         // In php neither == nor === compare arrays the way you want. The following
1426         // ensures that the arrays have the same keys, with the same values.
1427         $result = false;
1428         $diff1 = array_diff_assoc($state->responses, $teststate->responses);
1429         if (empty($diff1)) {
1430             $diff2 = array_diff_assoc($teststate->responses, $state->responses);
1431             $result =  empty($diff2);
1432         }
1434         return $result;
1435     }
1437     /**
1438     * Checks whether a response matches a given answer
1439     *
1440     * This method only applies to questions that use teacher-defined answers
1441     *
1442     * @return boolean
1443     */
1444     function test_response(&$question, &$state, $answer) {
1445         $response = isset($state->responses['']) ? $state->responses[''] : '';
1446         return ($response == $answer->answer);
1447     }
1449     /**
1450     * Performs response processing and grading
1451     *
1452     * This function performs response processing and grading and updates
1453     * the state accordingly.
1454     * @return boolean         Indicates success or failure.
1455     * @param object $question The question to be graded. Question type
1456     *                         specific information is included.
1457     * @param object $state    The state of the question to grade. The current
1458     *                         responses are in ->responses. The last graded state
1459     *                         is in ->last_graded (hence the most recently graded
1460     *                         responses are in ->last_graded->responses). The
1461     *                         question type specific information is also
1462     *                         included. The ->raw_grade and ->penalty fields
1463     *                         must be updated. The method is able to
1464     *                         close the question session (preventing any further
1465     *                         attempts at this question) by setting
1466     *                         $state->event to QUESTION_EVENTCLOSEANDGRADE
1467     * @param object $cmoptions
1468     */
1469     function grade_responses(&$question, &$state, $cmoptions) {
1470         // The default implementation uses the test_response method to
1471         // compare what the student entered against each of the possible
1472         // answers stored in the question, and uses the grade from the
1473         // first one that matches. It also sets the marks and penalty.
1474         // This should be good enought for most simple question types.
1476         $state->raw_grade = 0;
1477         foreach($question->options->answers as $answer) {
1478             if($this->test_response($question, $state, $answer)) {
1479                 $state->raw_grade = $answer->fraction;
1480                 break;
1481             }
1482         }
1484         // Make sure we don't assign negative or too high marks.
1485         $state->raw_grade = min(max((float) $state->raw_grade,
1486                             0.0), 1.0) * $question->maxgrade;
1488         // Update the penalty.
1489         $state->penalty = $question->penalty * $question->maxgrade;
1491         // mark the state as graded
1492         $state->event = ($state->event ==  QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
1494         return true;
1495     }
1497     /**
1498     * Returns true if the editing wizard is finished, false otherwise.
1499     *
1500     * The default implementation returns true, which is suitable for all question-
1501     * types that only use one editing form. This function is used in
1502     * question.php to decide whether we can regrade any states of the edited
1503     * question and redirect to edit.php.
1504     *
1505     * The dataset dependent question-type, which is extended by the calculated
1506     * question-type, overwrites this method because it uses multiple pages (i.e.
1507     * a wizard) to set up the question and associated datasets.
1508     *
1509     * @param object $form  The data submitted by the previous page.
1510     *
1511     * @return boolean      Whether the wizard's last page was submitted or not.
1512     */
1513     function finished_edit_wizard(&$form) {
1514         //In the default case there is only one edit page.
1515         return true;
1516     }
1518     /**
1519      * Call format_text from weblib.php with the options appropriate to question types.
1520      *
1521      * @param string $text the text to format.
1522      * @param integer $text the type of text. Normally $question->questiontextformat.
1523      * @param object $cmoptions the context the string is being displayed in. Only $cmoptions->course is used.
1524      * @return string the formatted text.
1525      */
1526     function format_text($text, $textformat, $cmoptions = NULL) {
1527         $formatoptions = new stdClass;
1528         $formatoptions->noclean = true;
1529         $formatoptions->para = false;
1530         return format_text($text, $textformat, $formatoptions, $cmoptions === NULL ? NULL : $cmoptions->course);
1531     }
1533     /*
1534      * Find all course / site files linked from a question.
1535      *
1536      * Need to check for links to files in question_answers.answer and feedback
1537      * and in question table in generalfeedback and questiontext fields. Methods
1538      * on child classes will also check extra question specific fields.
1539      *
1540      * Needs to be overriden for child classes that have extra fields containing
1541      * html.
1542      *
1543      * @param string html the html to search
1544      * @param int courseid search for files for courseid course or set to siteid for
1545      *              finding site files.
1546      * @return array of url, relative url is key and array with one item = question id as value
1547      *                  relative url is relative to course/site files directory root.
1548      */
1549     function find_file_links($question, $courseid){
1550         $urls = array();
1551     /// Questiontext and general feedback.
1552         $urls += question_find_file_links_from_html($question->questiontext, $courseid);
1553         $urls += question_find_file_links_from_html($question->generalfeedback, $courseid);
1555     /// Answers, if this question uses them.
1556         if (isset($question->options->answers)){
1557             foreach ($question->options->answers as $answerkey => $answer){
1558             /// URLs in the answers themselves, if appropriate.
1559                 if ($this->has_html_answers()) {
1560                     $urls += question_find_file_links_from_html($answer->answer, $courseid);
1561                 }
1562             /// URLs in the answer feedback.
1563                 $urls += question_find_file_links_from_html($answer->feedback, $courseid);
1564             }
1565         }
1567     /// Set all the values of the array to the question object
1568         if ($urls){
1569             $urls = array_combine(array_keys($urls), array_fill(0, count($urls), array($question->id)));
1570         }
1571         return $urls;
1572     }
1573     /*
1574      * Find all course / site files linked from a question.
1575      *
1576      * Need to check for links to files in question_answers.answer and feedback
1577      * and in question table in generalfeedback and questiontext fields. Methods
1578      * on child classes will also check extra question specific fields.
1579      *
1580      * Needs to be overriden for child classes that have extra fields containing
1581      * html.
1582      *
1583      * @param string html the html to search
1584      * @param int course search for files for courseid course or set to siteid for
1585      *              finding site files.
1586      * @return array of files, file name is key and array with one item = question id as value
1587      */
1588     function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){
1589         global $CFG, $DB;
1590         $updateqrec = false;
1591     /// Questiontext and general feedback.
1592         $question->questiontext = question_replace_file_links_in_html($question->questiontext, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
1593         $question->generalfeedback = question_replace_file_links_in_html($question->generalfeedback, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
1595     /// If anything has changed, update it in the database.
1596         if ($updateqrec){
1597             $DB->update_record('question', $question);
1598         }
1601     /// Answers, if this question uses them.
1602         if (isset($question->options->answers)){
1603             //answers that do not need updating have been unset
1604             foreach ($question->options->answers as $answer){
1605                 $answerchanged = false;
1606             /// URLs in the answers themselves, if appropriate.
1607                 if ($this->has_html_answers()) {
1608                     $answer->answer = question_replace_file_links_in_html($answer->answer, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
1609                 }
1610             /// URLs in the answer feedback.
1611                 $answer->feedback = question_replace_file_links_in_html($answer->feedback, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
1612             /// If anything has changed, update it in the database.
1613                 if ($answerchanged){
1614                     $DB->update_record('question_answers', $answer);
1615                 }
1616             }
1617         }
1618     }
1620     /**
1621      * @return the best link to pass to print_error.
1622      * @param $cmoptions as passed in from outside.
1623      */
1624     function error_link($cmoptions) {
1625         global $CFG;
1626         $cm = get_coursemodule_from_instance('quiz', $cmoptions->id);
1627         if (!empty($cm->id)) {
1628             return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
1629         } else if (!empty($cm->course)) {
1630             return $CFG->wwwroot . '/course/view.php?id=' . $cm->course;
1631         } else {
1632             return '';
1633         }
1634     }
1636 /// RESTORE FUNCTIONS /////////////////
1638     /*
1639      * Restores the data in the question
1640      *
1641      * This is used in question/restorelib.php
1642      */
1643     function restore($old_question_id,$new_question_id,$info,$restore) {
1644         global $DB;
1646         $status = true;
1647         $extraquestionfields = $this->extra_question_fields();
1649         if (is_array($extraquestionfields)) {
1650             $questionextensiontable = array_shift($extraquestionfields);
1651             $tagname = strtoupper($this->name());
1652             $recordinfo = $info['#'][$tagname][0];
1654             $record = new stdClass;
1655             $qidcolname = $this->questionid_column_name();
1656             $record->$qidcolname = $new_question_id;
1657             foreach ($extraquestionfields as $field) {
1658                 $record->$field = backup_todb($recordinfo['#'][strtoupper($field)]['0']['#']);
1659             }
1660             $DB->insert_record($questionextensiontable, $record);
1661         }
1662         //TODO restore extra data in answers
1663         return $status;
1664     }
1666     function restore_map($old_question_id,$new_question_id,$info,$restore) {
1667         // There is nothing to decode
1668         return true;
1669     }
1671     function restore_recode_answer($state, $restore) {
1672         // There is nothing to decode
1673         return $state->answer;
1674     }
1676 /// IMPORT/EXPORT FUNCTIONS /////////////////
1678     /*
1679      * Imports question from the Moodle XML format
1680      *
1681      * Imports question using information from extra_question_fields function
1682      * If some of you fields contains id's you'll need to reimplement this
1683      */
1684     function import_from_xml($data, $question, $format, $extra=null) {
1685         $question_type = $data['@']['type'];
1686         if ($question_type != $this->name()) {
1687             return false;
1688         }
1690         $extraquestionfields = $this->extra_question_fields();
1691         if (!is_array($extraquestionfields)) {
1692             return false;
1693         }
1695         //omit table name
1696         array_shift($extraquestionfields);
1697         $qo = $format->import_headers($data);
1698         $qo->qtype = $question_type;
1700         foreach ($extraquestionfields as $field) {
1701             $qo->$field = $format->getpath($data, array('#',$field,0,'#'), $qo->$field);
1702         }
1704         // run through the answers
1705         $answers = $data['#']['answer'];
1706         $a_count = 0;
1707         $extraasnwersfields = $this->extra_answer_fields();
1708         if (is_array($extraasnwersfields)) {
1709             //TODO import the answers, with any extra data.
1710         } else {
1711             foreach ($answers as $answer) {
1712                 $ans = $format->import_answer($answer);
1713                 $qo->answer[$a_count] = $ans->answer;
1714                 $qo->fraction[$a_count] = $ans->fraction;
1715                 $qo->feedback[$a_count] = $ans->feedback;
1716                 ++$a_count;
1717             }
1718         }
1719         return $qo;
1720     }
1722     /*
1723      * Export question to the Moodle XML format
1724      *
1725      * Export question using information from extra_question_fields function
1726      * If some of you fields contains id's you'll need to reimplement this
1727      */
1728     function export_to_xml($question, $format, $extra=null) {
1729         $extraquestionfields = $this->extra_question_fields();
1730         if (!is_array($extraquestionfields)) {
1731             return false;
1732         }
1734         //omit table name
1735         array_shift($extraquestionfields);
1736         $expout='';
1737         foreach ($extraquestionfields as $field) {
1738             $exportedvalue = $question->options->$field;
1739             if (!empty($exportedvalue) && htmlspecialchars($exportedvalue) != $exportedvalue) {
1740                 $exportedvalue = '<![CDATA[' . $exportedvalue . ']]>';
1741             }
1742             $expout .= "    <$field>{$exportedvalue}</$field>\n";
1743         }
1745         $extraasnwersfields = $this->extra_answer_fields();
1746         if (is_array($extraasnwersfields)) {
1747             //TODO export answers with any extra data
1748         } else {
1749             foreach ($question->options->answers as $answer) {
1750                 $percent = 100 * $answer->fraction;
1751                 $expout .= "    <answer fraction=\"$percent\">\n";
1752                 $expout .= $format->writetext($answer->answer, 3, false);
1753                 $expout .= "      <feedback>\n";
1754                 $expout .= $format->writetext($answer->feedback, 4, false);
1755                 $expout .= "      </feedback>\n";
1756                 $expout .= "    </answer>\n";
1757             }
1758         }
1759         return $expout;
1760     }
1762     /**
1763      * Abstract function implemented by each question type. It runs all the code
1764      * required to set up and save a question of any type for testing purposes.
1765      * Alternate DB table prefix may be used to facilitate data deletion.
1766      */
1767     function generate_test($name, $courseid=null) {
1768         $form = new stdClass();
1769         $form->name = $name;
1770         $form->questiontextformat = 1;
1771         $form->questiontext = 'test question, generated by script';
1772         $form->defaultgrade = 1;
1773         $form->penalty = 0.1;
1774         $form->generalfeedback = "Well done";
1776         $context = get_context_instance(CONTEXT_COURSE, $courseid);
1777         $newcategory = question_make_default_categories(array($context));
1778         $form->category = $newcategory->id . ',1';
1780         $question = new stdClass();
1781         $question->courseid = $courseid;
1782         $question->qtype = $this->qtype;
1783         return array($form, $question);
1784     }
1786     /**
1787      * Get question context by category id
1788      * @param int $category
1789      * @return object $context
1790      */
1791     function get_context_by_category_id($category) {
1792         global $DB;
1793         $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$category));
1794         $context = get_context_instance_by_id($contextid);
1795         return $context;
1796     }
1798     /**
1799      * When move the category of questions, the belonging files should be moved as well
1800      * @param object $question, question information
1801      * @param object $newcategory, target category information
1802      */
1803     function move_files($question, $newcategory) {
1804         global $DB;
1805         $fs = get_file_storage();
1806         $component = 'question';
1807         // process general question files
1808         // Currently we have questiontext and generalfeedback areas
1809         foreach (array('questiontext', 'generalfeedback') as $filearea) {
1810             $files = $fs->get_area_files($question->contextid, $component, $filearea, $question->id);
1811             foreach ($files as $storedfile) {
1812                 if (!$storedfile->is_directory()) {
1813                     if ($newcategory->contextid == $question->contextid) {
1814                         continue;
1815                     }
1816                     $newfile = new object();
1817                     // only contextid changed
1818                     $newfile->contextid = (int)$newcategory->contextid;
1819                     $fs->create_file_from_storedfile($newfile, $storedfile);
1820                     // delete old files
1821                     $storedfile->delete();
1822                 }
1823             }
1824         }
1825     }