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