MDL-16263 A way for students to flag/bookmark, particular questions during a quiz...
[moodle.git] / question / type / questiontype.php
1 <?php  // $Id$
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      * The name this question should appear as in the create new question
48      * dropdown.
49      *
50      * @return mixed the desired string, or false to hide this question type in the menu.
51      */
52     function menu_name() {
53         $name = $this->name();
54         $menu_name = get_string($name, 'qtype_' . $name);
55         if ($menu_name[0] == '[') {
56             // Legacy behavior, if the string was not in the proper qtype_name
57             // language file, look it up in the quiz one.
58             $menu_name = get_string($name, 'quiz');
59         }
60         return $menu_name;
61     }
63     /**
64      * @return boolean true if this question can only be graded manually.
65      */
66     function is_manual_graded() {
67         return false;
68     }
70     /**
71      * @return boolean true if this question type can be used by the random question type.
72      */
73     function is_usable_by_random() {
74         return true;
75     }
77     /**
78      * @return whether the question_answers.answer field needs to have
79      * restore_decode_content_links_worker called on it.
80      */
81     function has_html_answers() {
82         return false;
83     }
85     /**
86      * If your question type has a table that extends the question table, and
87      * you want the base class to automatically save, backup and restore the extra fields,
88      * override this method to return an array wherer the first element is the table name,
89      * and the subsequent entries are the column names (apart from id and questionid).
90      *
91      * @return mixed array as above, or null to tell the base class to do nothing.
92      */
93     function extra_question_fields() {
94         return null;
95     }
97     /**
98      * If your question type has a table that extends the question_answers table,
99      * make this method return an array wherer the first element is the table name,
100      * and the subsequent entries are the column names (apart from id and answerid).
101      *
102      * @return mixed array as above, or null to tell the base class to do nothing.
103      */
104     function extra_answer_fields() {
105         return null;
106     }
108     /**
109      * Return an instance of the question editing form definition. This looks for a
110      * class called edit_{$this->name()}_question_form in the file
111      * {$CFG->docroot}/question/type/{$this->name()}/edit_{$this->name()}_question_form.php
112      * and if it exists returns an instance of it.
113      *
114      * @param string $submiturl passed on to the constructor call.
115      * @return object an instance of the form definition, or null if one could not be found.
116      */
117     function create_editing_form($submiturl, $question, $category, $contexts, $formeditable) {
118         global $CFG;
119         require_once("{$CFG->dirroot}/question/type/edit_question_form.php");
120         $definition_file = $CFG->dirroot.'/question/type/'.$this->name().'/edit_'.$this->name().'_form.php';
121         if (!(is_readable($definition_file) && is_file($definition_file))) {
122             return null;
123         }
124         require_once($definition_file);
125         $classname = 'question_edit_'.$this->name().'_form';
126         if (!class_exists($classname)) {
127             return null;
128         }
129         return new $classname($submiturl, $question, $category, $contexts, $formeditable);
130     }
132     /**
133      * @return string the full path of the folder this plugin's files live in.
134      */
135     function plugin_dir() {
136         global $CFG;
137         return $CFG->dirroot . '/question/type/' . $this->name();
138     }
140     /**
141      * @return string the URL of the folder this plugin's files live in.
142      */
143     function plugin_baseurl() {
144         global $CFG;
145         return $CFG->wwwroot . '/question/type/' . $this->name();
146     }
148     /**
149      * This method should be overriden if you want to include a special heading or some other
150      * html on a question editing page besides the question editing form.
151      *
152      * @param question_edit_form $mform a child of question_edit_form
153      * @param object $question
154      * @param string $wizardnow is '' for first page.
155      */
156     function display_question_editing_page(&$mform, $question, $wizardnow){
157         list($heading, $langmodule) = $this->get_heading(empty($question->id));
158         print_heading_with_help($heading, $this->name(), $langmodule);
159         $permissionstrs = array();
160         if (!empty($question->id)){
161             if ($question->formoptions->canedit){
162                 $permissionstrs[] = get_string('permissionedit', 'question');
163             }
164             if ($question->formoptions->canmove){
165                 $permissionstrs[] = get_string('permissionmove', 'question');
166             }
167             if ($question->formoptions->cansaveasnew){
168                 $permissionstrs[] = get_string('permissionsaveasnew', 'question');
169             }
170         }
171         if (!$question->formoptions->movecontext  && count($permissionstrs)){
172             print_heading(get_string('permissionto', 'question'), 'center', 3);
173             $html = '<ul>';
174             foreach ($permissionstrs as $permissionstr){
175                 $html .= '<li>'.$permissionstr.'</li>';
176             }
177             $html .= '</ul>';
178             print_box($html, 'boxwidthnarrow boxaligncenter generalbox');
179         }
180         $mform->display();
181     }
183     /**
184      * Method called by display_question_editing_page and by question.php to get heading for breadcrumbs.
185      *
186      * @return array a string heading and the langmodule in which it was found.
187      */
188     function get_heading($adding = false){
189         $name = $this->name();
190         $langmodule = 'qtype_' . $name;
191         if (!$adding){
192             $strtoget = 'editing' . $name;
193         } else {
194             $strtoget = 'adding' . $name;
195         }
196         $strheading = get_string($strtoget, $langmodule);
197         if ($strheading[0] == '[') {
198             // Legacy behavior, if the string was not in the proper qtype_name
199             // language file, look it up in the quiz one.
200             $langmodule = 'quiz';
201             $strheading = get_string($strtoget, $langmodule);
202         }
203         return array($strheading, $langmodule);
204     }
206     /**
207      *
208      *
209      * @param $question
210      */
211     function set_default_options(&$question) {
212     }
214     /**
215     * Saves or updates a question after editing by a teacher
216     *
217     * Given some question info and some data about the answers
218     * this function parses, organises and saves the question
219     * It is used by {@link question.php} when saving new data from
220     * a form, and also by {@link import.php} when importing questions
221     * This function in turn calls {@link save_question_options}
222     * to save question-type specific options
223     * @param object $question the question object which should be updated
224     * @param object $form the form submitted by the teacher
225     * @param object $course the course we are in
226     * @return object On success, return the new question object. On failure,
227     *       return an object as follows. If the error object has an errors field,
228     *       display that as an error message. Otherwise, the editing form will be
229     *       redisplayed with validation errors, from validation_errors field, which
230     *       is itself an object, shown next to the form fields.
231     */
232     function save_question($question, $form, $course) {
233         global $USER, $DB;
234         // This default implementation is suitable for most
235         // question types.
237         // First, save the basic question itself
238         $question->name               = trim($form->name);
239         $question->questiontext       = trim($form->questiontext);
240         $question->questiontextformat = $form->questiontextformat;
241         $question->parent             = isset($form->parent)? $form->parent : 0;
242         $question->length = $this->actual_number_of_questions($question);
243         $question->penalty = isset($form->penalty) ? $form->penalty : 0;
245         if (empty($form->image)) {
246             $question->image = "";
247         } else {
248             $question->image = $form->image;
249         }
251         if (empty($form->generalfeedback)) {
252             $question->generalfeedback = '';
253         } else {
254             $question->generalfeedback = trim($form->generalfeedback);
255         }
257         if (empty($question->name)) {
258             $question->name = shorten_text(strip_tags($question->questiontext), 15);
259             if (empty($question->name)) {
260                 $question->name = '-';
261             }
262         }
264         if ($question->penalty > 1 or $question->penalty < 0) {
265             $question->errors['penalty'] = get_string('invalidpenalty', 'quiz');
266         }
268         if (isset($form->defaultgrade)) {
269             $question->defaultgrade = $form->defaultgrade;
270         }
272         if (!empty($question->id)) { // Question already exists
273             if (isset($form->categorymoveto)){
274                 question_require_capability_on($question, 'move');
275                 list($question->category, $movetocontextid) = explode(',', $form->categorymoveto);
276                 //don't need to test add permission of category we are moving question to.
277                 //Only categories that we have permission to add
278                 //a question to will get through the form cleaning code for the select box.
279             }
280             // keep existing unique stamp code
281             $question->stamp = $DB->get_field('question', 'stamp', array('id' => $question->id));
282             $question->modifiedby = $USER->id;
283             $question->timemodified = time();
284             if (!$DB->update_record('question', $question)) {
285                 print_error('cannotupdatequestion', 'question');
286             }
287         } else {         // Question is a new one
288             if (isset($form->categorymoveto)){
289                 // Doing save as new question, and we have move rights.
290                 list($question->category, $notused) = explode(',', $form->categorymoveto);
291                 //don't need to test add permission of category we are moving question to.
292                 //Only categories that we have permission to add
293                 //a question to will get through the form cleaning code for the select box.
294             } else {
295                 // Really a new question.
296                 list($question->category, $notused) = explode(',', $form->category);
297             }
298             // Set the unique code
299             $question->stamp = make_unique_id_code();
300             $question->createdby = $USER->id;
301             $question->modifiedby = $USER->id;
302             $question->timecreated = time();
303             $question->timemodified = time();
304             if (!$question->id = $DB->insert_record('question', $question)) {
305                 print_error('cannotinsertquestion', 'question');
306             }
307         }
309         // Now to save all the answers and type-specific options
311         $form->id = $question->id;
312         $form->qtype = $question->qtype;
313         $form->category = $question->category;
314         $form->questiontext = $question->questiontext;
316         $result = $this->save_question_options($form);
318         if (!empty($result->error)) {
319             print_error('questionsaveerror', 'question', '', $result->error);
320         }
322         if (!empty($result->notice)) {
323             notice($result->notice, "question.php?id=$question->id");
324         }
326         if (!empty($result->noticeyesno)) {
327             notice_yesno($result->noticeyesno, "question.php?id=$question->id&amp;courseid={$course->id}",
328                 "edit.php?courseid={$course->id}");
329             print_footer($course);
330             exit;
331         }
333         // Give the question a unique version stamp determined by question_hash()
334         if (!$DB->set_field('question', 'version', question_hash($question), array('id' => $question->id))) {
335             print_error('cannotupdatequestionver', 'question');
336         }
338         return $question;
339     }
341     /**
342     * Saves question-type specific options
343     *
344     * This is called by {@link save_question()} to save the question-type specific data
345     * @return object $result->error or $result->noticeyesno or $result->notice
346     * @param object $question  This holds the information from the editing form,
347     *                          it is not a standard question object.
348     */
349     function save_question_options($question) {
350         global $DB;
351         $extra_question_fields = $this->extra_question_fields();
353         if (is_array($extra_question_fields)) {
354             $question_extension_table = array_shift($extra_question_fields);
356             $function = 'update_record';
357             $options = $DB->get_record($question_extension_table, array('questionid' => $question->id));
358             if (!$options) {
359                 $function = 'insert_record';
360                 $options = new stdClass;
361                 $options->questionid = $question->id;
362             }
363             foreach ($extra_question_fields as $field) {
364                 if (!isset($question->$field)) {
365                     $result = new stdClass;
366                     $result->error = "No data for field $field when saving " .
367                             $this->name() . ' question id ' . $question->id;
368                     return $result;
369                 }
370                 $options->$field = $question->$field;
371             }
373             if (!$DB->{$function}($question_extension_table, $options)) {
374                 $result = new stdClass;
375                 $result->error = 'Could not save question options for ' .
376                         $this->name() . ' question id ' . $question->id;
377                 return $result;
378             }
379         }
381         $extra_answer_fields = $this->extra_answer_fields();
382         // TODO save the answers, with any extra data.
384         return null;
385     }
387     /**
388     * Changes all states for the given attempts over to a new question
389     *
390     * This is used by the versioning code if the teacher requests that a question
391     * gets replaced by the new version. In order for the attempts to be regraded
392     * properly all data in the states referring to the old question need to be
393     * changed to refer to the new version instead. In particular for question types
394     * that use the answers table the answers belonging to the old question have to
395     * be changed to those belonging to the new version.
396     *
397     * @param integer $oldquestionid  The id of the old question
398     * @param object $newquestion    The new question
399     * @param array  $attempts       An array of all attempt objects in whose states
400     *                               replacement should take place
401     */
402     function replace_question_in_attempts($oldquestionid, $newquestion, $attemtps) {
403         echo 'Not yet implemented';
404         return;
405     }
407     /**
408     * Loads the question type specific options for the question.
409     *
410     * This function loads any question type specific options for the
411     * question from the database into the question object. This information
412     * is placed in the $question->options field. A question type is
413     * free, however, to decide on a internal structure of the options field.
414     * @return bool            Indicates success or failure.
415     * @param object $question The question object for the question. This object
416     *                         should be updated to include the question type
417     *                         specific information (it is passed by reference).
418     */
419     function get_question_options(&$question) {
420         global $CFG, $DB;
422         if (!isset($question->options)) {
423             $question->options = new object;
424         }
426         $extra_question_fields = $this->extra_question_fields();
427         if (is_array($extra_question_fields)) {
428             $question_extension_table = array_shift($extra_question_fields);
429             $extra_data = $DB->get_record($question_extension_table, array('questionid' => $question->id), '', implode(', ', $extra_question_fields));
430             if ($extra_data) {
431                 foreach ($extra_question_fields as $field) {
432                     $question->options->$field = $extra_data->$field;
433                 }
434             } else {
435                 notify("Failed to load question options from the table $question_extension_table for questionid " .
436                         $question->id);
437                 return false;
438             }
439         }
441         $extra_answer_fields = $this->extra_answer_fields();
442         if (is_array($extra_answer_fields)) {
443             $answer_extension_table = array_shift($extra_answer_fields);
444             $question->options->answers = $DB->get_records_sql("
445                     SELECT qa.*, qax." . implode(', qax.', $extra_answer_fields) . "
446                     FROM {question_answers} qa, {$answer_extension_table} qax
447                     WHERE qa.questionid = ? AND qax.answerid = qa.id", array($question->id));
448             if (!$question->options->answers) {
449                 notify("Failed to load question answers from the table $answer_extension_table for questionid " .
450                         $question->id);
451                 return false;
452             }
453         } else {
454             // Don't check for success or failure because some question types do not use the answers table.
455             $question->options->answers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC');
456         }
458         return true;
459     }
461     /**
462     * Deletes states from the question-type specific tables
463     *
464     * @param string $stateslist  Comma separated list of state ids to be deleted
465     */
466     function delete_states($stateslist) {
467         /// The default question type does not have any tables of its own
468         // therefore there is nothing to delete
470         return true;
471     }
473     /**
474     * Deletes a question from the question-type specific tables
475     *
476     * @return boolean Success/Failure
477     * @param object $question  The question being deleted
478     */
479     function delete_question($questionid) {
480         global $CFG, $DB;
481         $success = true;
483         $extra_question_fields = $this->extra_question_fields();
484         if (is_array($extra_question_fields)) {
485             $question_extension_table = array_shift($extra_question_fields);
486             $success = $success && $DB->delete_records($question_extension_table, array('questionid' => $questionid));
487         }
489         $extra_answer_fields = $this->extra_answer_fields();
490         if (is_array($extra_answer_fields)) {
491             $answer_extension_table = array_shift($extra_answer_fields);
492             $success = $success && $DB->delete_records_select($answer_extension_table,
493                 "answerid IN (SELECT qa.id FROM {question_answers} qa WHERE qa.question = ?)", array($questionid));
494         }
496         $success = $success && $DB->delete_records('question_answers', array('question' => $questionid));
498         return $success;
499     }
501     /**
502     * Returns the number of question numbers which are used by the question
503     *
504     * This function returns the number of question numbers to be assigned
505     * to the question. Most question types will have length one; they will be
506     * assigned one number. The 'description' type, however does not use up a
507     * number and so has a length of zero. Other question types may wish to
508     * handle a bundle of questions and hence return a number greater than one.
509     * @return integer         The number of question numbers which should be
510     *                         assigned to the question.
511     * @param object $question The question whose length is to be determined.
512     *                         Question type specific information is included.
513     */
514     function actual_number_of_questions($question) {
515         // By default, each question is given one number
516         return 1;
517     }
519     /**
520     * Creates empty session and response information for the question
521     *
522     * This function is called to start a question session. Empty question type
523     * specific session data (if any) and empty response data will be added to the
524     * state object. Session data is any data which must persist throughout the
525     * attempt possibly with updates as the user interacts with the
526     * question. This function does NOT create new entries in the database for
527     * the session; a call to the {@link save_session_and_responses} member will
528     * occur to do this.
529     * @return bool            Indicates success or failure.
530     * @param object $question The question for which the session is to be
531     *                         created. Question type specific information is
532     *                         included.
533     * @param object $state    The state to create the session for. Note that
534     *                         this will not have been saved in the database so
535     *                         there will be no id. This object will be updated
536     *                         to include the question type specific information
537     *                         (it is passed by reference). In particular, empty
538     *                         responses will be created in the ->responses
539     *                         field.
540     * @param object $cmoptions
541     * @param object $attempt  The attempt for which the session is to be
542     *                         started. Questions may wish to initialize the
543     *                         session in different ways depending on the user id
544     *                         or time available for the attempt.
545     */
546     function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
547         // The default implementation should work for the legacy question types.
548         // Most question types with only a single form field for the student's response
549         // will use the empty string '' as the index for that one response. This will
550         // automatically be stored in and restored from the answer field in the
551         // question_states table.
552         $state->responses = array(
553                 '' => '',
554         );
555         return true;
556     }
558     /**
559     * Restores the session data and most recent responses for the given state
560     *
561     * This function loads any session data associated with the question
562     * session in the given state from the database into the state object.
563     * In particular it loads the responses that have been saved for the given
564     * state into the ->responses member of the state object.
565     *
566     * Question types with only a single form field for the student's response
567     * will not need not restore the responses; the value of the answer
568     * field in the question_states table is restored to ->responses['']
569     * before this function is called. Question types with more response fields
570     * should override this method and set the ->responses field to an
571     * associative array of responses.
572     * @return bool            Indicates success or failure.
573     * @param object $question The question object for the question including any
574     *                         question type specific information.
575     * @param object $state    The saved state to load the session for. This
576     *                         object should be updated to include the question
577     *                         type specific session information and responses
578     *                         (it is passed by reference).
579     */
580     function restore_session_and_responses(&$question, &$state) {
581         // The default implementation does nothing (successfully)
582         return true;
583     }
585     /**
586     * Saves the session data and responses for the given question and state
587     *
588     * This function saves the question type specific session data from the
589     * state object to the database. In particular for most question types it saves the
590     * responses from the ->responses member of the state object. The question type
591     * non-specific data for the state has already been saved in the question_states
592     * table and the state object contains the corresponding id and
593     * sequence number which may be used to index a question type specific table.
594     *
595     * Question types with only a single form field for the student's response
596     * which is contained in ->responses[''] will not have to save this response,
597     * it will already have been saved to the answer field of the question_states table.
598     * Question types with more response fields should override this method to convert
599     * the data the ->responses array into a single string field, and save it in the
600     * database. The implementation in the multichoice question type is a good model to follow.
601     * http://cvs.moodle.org/contrib/plugins/question/type/opaque/questiontype.php?view=markup
602     * has a solution that is probably quite generally applicable.
603     * @return bool            Indicates success or failure.
604     * @param object $question The question object for the question including
605     *                         the question type specific information.
606     * @param object $state    The state for which the question type specific
607     *                         data and responses should be saved.
608     */
609     function save_session_and_responses(&$question, &$state) {
610         // The default implementation does nothing (successfully)
611         return true;
612     }
614     /**
615     * Returns an array of values which will give full marks if graded as
616     * the $state->responses field
617     *
618     * The correct answer to the question in the given state, or an example of
619     * a correct answer if there are many, is returned. This is used by some question
620     * types in the {@link grade_responses()} function but it is also used by the
621     * question preview screen to fill in correct responses.
622     * @return mixed           A response array giving the responses corresponding
623     *                         to the (or a) correct answer to the question. If there is
624     *                         no correct answer that scores 100% then null is returned.
625     * @param object $question The question for which the correct answer is to
626     *                         be retrieved. Question type specific information is
627     *                         available.
628     * @param object $state    The state of the question, for which a correct answer is
629     *                         needed. Question type specific information is included.
630     */
631     function get_correct_responses(&$question, &$state) {
632         /* The default implementation returns the response for the first answer
633         that gives full marks. */
634         if ($question->options->answers) {
635             foreach ($question->options->answers as $answer) {
636                 if (((int) $answer->fraction) === 1) {
637                     return array('' => $answer->answer);
638                 }
639             }
640         }
641         return null;
642     }
644     /**
645     * Return an array of values with the texts for all possible responses stored
646     * for the question
647     *
648     * All answers are found and their text values isolated
649     * @return object          A mixed object
650     *             ->id        question id. Needed to manage random questions:
651     *                         it's the id of the actual question presented to user in a given attempt
652     *             ->responses An array of values giving the responses corresponding
653     *                         to all answers to the question. Answer ids are used as keys.
654     *                         The text and partial credit are the object components
655     * @param object $question The question for which the answers are to
656     *                         be retrieved. Question type specific information is
657     *                         available.
658     */
659     // ULPGC ecastro
660     function get_all_responses(&$question, &$state) {
661         if (isset($question->options->answers) && is_array($question->options->answers)) {
662             $answers = array();
663             foreach ($question->options->answers as $aid=>$answer) {
664                 $r = new stdClass;
665                 $r->answer = $answer->answer;
666                 $r->credit = $answer->fraction;
667                 $answers[$aid] = $r;
668             }
669             $result = new stdClass;
670             $result->id = $question->id;
671             $result->responses = $answers;
672             return $result;
673         } else {
674             return null;
675         }
676     }
677     
678     /**
679      * @param object $question
680      * @return mixed either a integer score out of 1 that the average random
681      * guess by a student might give or an empty string which means will not
682      * calculate.
683      */
684     function get_random_guess_score($question) {
685         return 0;
686     }
687     /**
688     * Return the actual response to the question in a given state
689     * for the question.
690     *
691     * @return mixed           An array containing the response or reponses (multiple answer, match)
692     *                         given by the user in a particular attempt.
693     * @param object $question The question for which the correct answer is to
694     *                         be retrieved. Question type specific information is
695     *                         available.
696     * @param object $state    The state object that corresponds to the question,
697     *                         for which a correct answer is needed. Question
698     *                         type specific information is included.
699     */
700     // ULPGC ecastro
701     function get_actual_response($question, $state) {
702        if (!empty($state->responses)) {
703            $responses[] = $state->responses[''];
704        } else {
705            $responses[] = '';
706        }
707        return $responses;
708     }
710     // ULPGC ecastro
711     function get_fractional_grade(&$question, &$state) {
712         $maxgrade = $question->maxgrade;
713         $grade = $state->grade;
714         if ($maxgrade) {
715             return (float)($grade/$maxgrade);
716         } else {
717             return (float)$grade;
718         }
719     }
722     /**
723     * Checks if the response given is correct and returns the id
724     *
725     * @return int             The ide number for the stored answer that matches the response
726     *                         given by the user in a particular attempt.
727     * @param object $question The question for which the correct answer is to
728     *                         be retrieved. Question type specific information is
729     *                         available.
730     * @param object $state    The state object that corresponds to the question,
731     *                         for which a correct answer is needed. Question
732     *                         type specific information is included.
733     */
734     // ULPGC ecastro
735     function check_response(&$question, &$state){
736         return false;
737     }
739     // Used by the following function, so that it only returns results once per quiz page.
740     var $already_done = false;
741     /**
742      * If this question type requires extra CSS or JavaScript to function,
743      * then this method will return an array of <link ...> tags that reference
744      * those stylesheets. This function will also call require_js()
745      * from ajaxlib.php, to get any necessary JavaScript linked in too.
746      *
747      * The two parameters match the first two parameters of print_question.
748      *
749      * @param object $question The question object.
750      * @param object $state    The state object.
751      *
752      * @return an array of bits of HTML to add to the head of pages where
753      * this question is print_question-ed in the body. The array should use
754      * integer array keys, which have no significance.
755      */
756     function get_html_head_contributions(&$question, &$state) {
757         // By default, we link to any of the files styles.css, styles.php,
758         // script.js or script.php that exist in the plugin folder.
759         // Core question types should not use this mechanism. Their styles
760         // should be included in the standard theme.
762         // We only do this once
763         // for this question type, no matter how often this method is called.
764         if ($this->already_done) {
765             return array();
766         }
767         $this->already_done = true;
769         $plugindir = $this->plugin_dir();
770         $baseurl = $this->plugin_baseurl();
771         $stylesheets = array();
772         if (file_exists($plugindir . '/styles.css')) {
773             $stylesheets[] = 'styles.css';
774         }
775         if (file_exists($plugindir . '/styles.php')) {
776             $stylesheets[] = 'styles.php';
777         }
778         if (file_exists($plugindir . '/script.js')) {
779             require_js($baseurl . '/script.js');
780         }
781         if (file_exists($plugindir . '/script.php')) {
782             require_js($baseurl . '/script.php');
783         }
784         $contributions = array();
785         foreach ($stylesheets as $stylesheet) {
786             $contributions[] = '<link rel="stylesheet" type="text/css" href="' .
787                     $baseurl . '/' . $stylesheet . '" />';
788         }
789         return $contributions;
790     }
792     /**
793      * Prints the question including the number, grading details, content,
794      * feedback and interactions
795      *
796      * This function prints the question including the question number,
797      * grading details, content for the question, any feedback for the previously
798      * submitted responses and the interactions. The default implementation calls
799      * various other methods to print each of these parts and most question types
800      * will just override those methods.
801      * @param object $question The question to be rendered. Question type
802      *                         specific information is included. The
803      *                         maximum possible grade is in ->maxgrade. The name
804      *                         prefix for any named elements is in ->name_prefix.
805      * @param object $state    The state to render the question in. The grading
806      *                         information is in ->grade, ->raw_grade and
807      *                         ->penalty. The current responses are in
808      *                         ->responses. This is an associative array (or the
809      *                         empty string or null in the case of no responses
810      *                         submitted). The last graded state is in
811      *                         ->last_graded (hence the most recently graded
812      *                         responses are in ->last_graded->responses). The
813      *                         question type specific information is also
814      *                         included.
815      * @param integer $number  The number for this question.
816      * @param object $cmoptions
817      * @param object $options  An object describing the rendering options.
818      */
819     function print_question(&$question, &$state, $number, $cmoptions, $options) {
820         /* The default implementation should work for most question types
821         provided the member functions it calls are overridden where required.
822         The layout is determined by the template question.html */
824         global $CFG;
825         $isgraded = question_state_is_graded($state->last_graded);
827         // get the context so we can determine whether some extra links
828         // should be shown.
829         if (!empty($cmoptions->id)) {
830             $cm = get_coursemodule_from_instance('quiz', $cmoptions->id);
831             $context = get_context_instance(CONTEXT_MODULE, $cm->id);
832             $cmorcourseid = '&amp;cmid='.$cm->id;
833         } else if (!empty($cmoptions->course)) {
834             $context = get_context_instance(CONTEXT_COURSE, $cmoptions->course);
835             $cmorcourseid = '&amp;courseid='.$cmoptions->course;
836         } else {
837             print_error('missingcourseorcmid', 'question');
838         }
840         // For editing teachers print a link to an editing popup window
841         $editlink = '';
842         if (question_has_capability_on($question, 'edit')) {
843             $stredit = get_string('edit');
844             $linktext = '<img src="'.$CFG->pixpath.'/t/edit.gif" alt="'.$stredit.'" />';
845             $editlink = link_to_popup_window('/question/question.php?inpopup=1&amp;id='.$question->id.$cmorcourseid,
846                                              'editquestion', $linktext, 450, 550, $stredit, '', true);
847         }
849         $generalfeedback = '';
850         if ($isgraded && $options->generalfeedback) {
851             $generalfeedback = $this->format_text($question->generalfeedback,
852                     $question->questiontextformat, $cmoptions);
853         }
855         $grade = '';
856         if ($question->maxgrade and $options->scores) {
857             if ($cmoptions->optionflags & QUESTION_ADAPTIVE) {
858                 if ($isgraded) {
859                     $grade = question_format_grade($cmoptions, $state->last_graded->grade).'/';
860                 } else {
861                     $grade = '--/'; 
862                 }
863             }
864             $grade .= question_format_grade($cmoptions, $question->maxgrade);
865         }
867         $comment = $state->manualcomment;
868         $commentlink = '';
870         if (isset($options->questioncommentlink) && $context && has_capability('mod/quiz:grade', $context)) {
871             $strcomment = get_string('commentorgrade', 'quiz');
872             $question_to_comment = isset($question->randomquestionid) ? $question->randomquestionid : $question->id;
873             $commentlink = '<div class="commentlink">'.link_to_popup_window ($options->questioncommentlink.'?attempt='.$state->attempt.'&amp;question='.$question_to_comment,
874                              'commentquestion', $strcomment, 450, 650, $strcomment, 'none', true).'</div>';
875         }
877         $history = $this->history($question, $state, $number, $cmoptions, $options);
879         include "$CFG->dirroot/question/type/question.html";
880     }
882     /**
883      * Render the question flag, assuming $flagsoption allows it. You will probably
884      * never need to override this method.
885      *
886      * @param object $question the question
887      * @param object $state its current state
888      * @param integer $flagsoption the option that says whether flags should be displayed.
889      */
890     protected function print_question_flag($question, $state, $flagsoption) {
891         global $CFG;
892         switch ($flagsoption) {
893             case QUESTION_FLAGSSHOWN:
894                 $flagcontent = $this->get_question_flag_tag($state->flagged);
895                 break;
896             case QUESTION_FLAGSEDITABLE:
897                 $id = $question->name_prefix . '_flagged';
898                 if ($state->flagged) {
899                     $checked = 'checked="checked" ';
900                 } else {
901                     $checked = '';
902                 }
903                 $qsid = $state->questionsessionid;
904                 $aid = $state->attempt;
905                 $qid = $state->question;
906                 $checksum = question_get_toggleflag_checksum($aid, $qid, $qsid);
907                 $postdata = "qsid=$qsid&amp;aid=$aid&amp;qid=$qid&amp;checksum=$checksum&amp;sesskey=" . sesskey();
908                 $flagcontent = '<input type="checkbox" id="' . $id . '" name="' . $id .
909                         '" value="1" ' . $checked . ' />' . 
910                         '<label for="' . $id . '">' . $this->get_question_flag_tag(
911                         $state->flagged, $id . 'img') . '</label>' .
912                         "\n" . '<script type="text/javascript">question_flag_changer.init_flag(' .
913                         "'$id', '$postdata');</script>";
914                 break;
915             default:
916                 $flagcontent = '';
917         }
918         if ($flagcontent) {
919             echo '<div class="questionflag">' . $flagcontent . "</div>\n";
920         }
921     }
923     /**
924      * Work out the actual img tag needed for the flag
925      *
926      * @param boolean $flagged whether the question is currently flagged.
927      * @param string $id an id to be added as an attribute to the img (optional). 
928      * @return string the img tag.
929      */
930     protected function get_question_flag_tag($flagged, $id = '') {
931         global $CFG;
932         if ($id) {
933             $id = 'id="' . $id . '" ';
934         }
935         if ($flagged) {
936             $img = 'flagged.png';
937         } else {
938             $img = 'unflagged.png';
939         }
940         return '<img ' . $id . 'src="' . $CFG->pixpath . '/i/' . $img .
941                 '" alt="' . get_string('flagthisquestion', 'question') . '" />';
942     }
944     /**
945      * Print history of responses
946      *
947      * Used by print_question()
948      */
949     function history($question, $state, $number, $cmoptions, $options) {
950         global $DB;
951         $history = '';
952         if(isset($options->history) and $options->history) {
953             if ($options->history == 'all') {
954                 // show all states
955                 $states = $DB->get_records_select('question_states', "attempt = ? AND question = ? AND event > '0'", array($state->attempt, $question->id), 'seq_number ASC');
956             } else {
957                 // show only graded states
958                 $states = $DB->get_records_select('question_states', "attempt = ? AND question = ? AND event IN (".QUESTION_EVENTS_GRADED.")", array($state->attempt, $question->id), 'seq_number ASC');
959             }
960             if (count($states) > 1) {
961                 $strreviewquestion = get_string('reviewresponse', 'quiz');
962                 $table = new stdClass;
963                 $table->width = '100%';
964                 if ($options->scores) {
965                     $table->head  = array (
966                                            get_string('numberabbr', 'quiz'),
967                                            get_string('action', 'quiz'),
968                                            get_string('response', 'quiz'),
969                                            get_string('time'),
970                                            get_string('score', 'quiz'),
971                                            //get_string('penalty', 'quiz'),
972                                            get_string('grade', 'quiz'),
973                                            );
974                 } else {
975                     $table->head  = array (
976                                            get_string('numberabbr', 'quiz'),
977                                            get_string('action', 'quiz'),
978                                            get_string('response', 'quiz'),
979                                            get_string('time'),
980                                            );
981                 }
983                 foreach ($states as $st) {
984                     $st->responses[''] = $st->answer;
985                     $this->restore_session_and_responses($question, $st);
986                     $b = ($state->id == $st->id) ? '<b>' : '';
987                     $be = ($state->id == $st->id) ? '</b>' : '';
988                     if ($state->id == $st->id) {
989                         $link = '<b>'.$st->seq_number.'</b>';
990                     } else {
991                         if(isset($options->questionreviewlink)) {
992                             $link = link_to_popup_window($options->questionreviewlink .
993                                     '&amp;question=' . $question->id . '&amp;state=' . $st->id,
994                                     'reviewquestion', $st->seq_number, 450, 650, $strreviewquestion,
995                                     'none', true);
996                         } else {
997                             $link = $st->seq_number;
998                         }
999                     }
1000                     if ($options->scores) {
1001                         $table->data[] = array (
1002                                                 $link,
1003                                                 $b.get_string('event'.$st->event, 'quiz').$be,
1004                                                 $b.$this->response_summary($question, $st).$be,
1005                                                 $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be,
1006                                                 $b.question_format_grade($cmoptions, $st->raw_grade).$be,
1007                                                 $b.question_format_grade($cmoptions, $st->grade).$be
1008                                                 );
1009                     } else {
1010                         $table->data[] = array (
1011                                                 $link,
1012                                                 $b.get_string('event'.$st->event, 'quiz').$be,
1013                                                 $b.$this->response_summary($question, $st).$be,
1014                                                 $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be,
1015                                                 );
1016                     }
1017                 }
1018                 $history = print_table($table, true);
1019             }
1020         }
1021         return $history;
1022     }
1025     /**
1026     * Prints the score obtained and maximum score available plus any penalty
1027     * information
1028     *
1029     * This function prints a summary of the scoring in the most recently
1030     * graded state (the question may not have been submitted for marking at
1031     * the current state). The default implementation should be suitable for most
1032     * question types.
1033     * @param object $question The question for which the grading details are
1034     *                         to be rendered. Question type specific information
1035     *                         is included. The maximum possible grade is in
1036     *                         ->maxgrade.
1037     * @param object $state    The state. In particular the grading information
1038     *                          is in ->grade, ->raw_grade and ->penalty.
1039     * @param object $cmoptions
1040     * @param object $options  An object describing the rendering options.
1041     */
1042     function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
1043         /* The default implementation prints the number of marks if no attempt
1044         has been made. Otherwise it displays the grade obtained out of the
1045         maximum grade available and a warning if a penalty was applied for the
1046         attempt and displays the overall grade obtained counting all previous
1047         responses (and penalties) */
1049         if (QUESTION_EVENTDUPLICATE == $state->event) {
1050             echo ' ';
1051             print_string('duplicateresponse', 'quiz');
1052         }
1053         if (!empty($question->maxgrade) && $options->scores) {
1054             if (question_state_is_graded($state->last_graded)) {
1055                 // Display the grading details from the last graded state
1056                 $grade = new stdClass;
1057                 $grade->cur = question_format_grade($cmoptions, $state->last_graded->grade);
1058                 $grade->max = question_format_grade($cmoptions, $question->maxgrade);
1059                 $grade->raw = question_format_grade($cmoptions, $state->last_graded->raw_grade);
1061                 // let student know wether the answer was correct
1062                 $class = question_get_feedback_class($state->last_graded->raw_grade / 
1063                         $question->maxgrade);
1064                 echo '<div class="correctness ' . $class . '">' . get_string($class, 'quiz') . '</div>';
1066                 echo '<div class="gradingdetails">';
1067                 // print grade for this submission
1068                 print_string('gradingdetails', 'quiz', $grade);
1069                 if ($cmoptions->penaltyscheme) {
1070                     // print details of grade adjustment due to penalties
1071                     if ($state->last_graded->raw_grade > $state->last_graded->grade){
1072                         echo ' ';
1073                         print_string('gradingdetailsadjustment', 'quiz', $grade);
1074                     }
1075                     // print info about new penalty
1076                     // penalty is relevant only if the answer is not correct and further attempts are possible
1077                     if (($state->last_graded->raw_grade < $question->maxgrade / 1.01)
1078                                 and (QUESTION_EVENTCLOSEANDGRADE !== $state->event)) {
1080                         if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) {
1081                             // A penalty was applied so display it
1082                             echo ' ';
1083                             print_string('gradingdetailspenalty', 'quiz', question_format_grade($cmoptions, $state->last_graded->penalty));
1084                         } else {
1085                             /* No penalty was applied even though the answer was
1086                             not correct (eg. a syntax error) so tell the student
1087                             that they were not penalised for the attempt */
1088                             echo ' ';
1089                             print_string('gradingdetailszeropenalty', 'quiz');
1090                         }
1091                     }
1092                 }
1093                 echo '</div>';
1094             }
1095         }
1096     }
1098     /**
1099     * Prints the main content of the question including any interactions
1100     *
1101     * This function prints the main content of the question including the
1102     * interactions for the question in the state given. The last graded responses
1103     * are printed or indicated and the current responses are selected or filled in.
1104     * Any names (eg. for any form elements) are prefixed with $question->name_prefix.
1105     * This method is called from the print_question method.
1106     * @param object $question The question to be rendered. Question type
1107     *                         specific information is included. The name
1108     *                         prefix for any named elements is in ->name_prefix.
1109     * @param object $state    The state to render the question in. The grading
1110     *                         information is in ->grade, ->raw_grade and
1111     *                         ->penalty. The current responses are in
1112     *                         ->responses. This is an associative array (or the
1113     *                         empty string or null in the case of no responses
1114     *                         submitted). The last graded state is in
1115     *                         ->last_graded (hence the most recently graded
1116     *                         responses are in ->last_graded->responses). The
1117     *                         question type specific information is also
1118     *                         included.
1119     *                         The state is passed by reference because some adaptive
1120     *                         questions may want to update it during rendering
1121     * @param object $cmoptions
1122     * @param object $options  An object describing the rendering options.
1123     */
1124     function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
1125         /* This default implementation prints an error and must be overridden
1126         by all question type implementations, unless the default implementation
1127         of print_question has been overridden. */
1129         notify('Error: Question formulation and input controls has not'
1130                .'  been implemented for question type '.$this->name());
1131     }
1133     /**
1134     * Prints the submit button(s) for the question in the given state
1135     *
1136     * This function prints the submit button(s) for the question in the
1137     * given state. The name of any button created will be prefixed with the
1138     * unique prefix for the question in $question->name_prefix. The suffix
1139     * 'submit' is reserved for the single question submit button and the suffix
1140     * 'validate' is reserved for the single question validate button (for
1141     * question types which support it). Other suffixes will result in a response
1142     * of that name in $state->responses which the printing and grading methods
1143     * can then use.
1144     * @param object $question The question for which the submit button(s) are to
1145     *                         be rendered. Question type specific information is
1146     *                         included. The name prefix for any
1147     *                         named elements is in ->name_prefix.
1148     * @param object $state    The state to render the buttons for. The
1149     *                         question type specific information is also
1150     *                         included.
1151     * @param object $cmoptions
1152     * @param object $options  An object describing the rendering options.
1153     */
1154     function print_question_submit_buttons(&$question, &$state, $cmoptions, $options) {
1155         // The default implementation should be suitable for most question types.
1156         // It prints a mark button in the case where individual marking is allowed.
1157         if (($cmoptions->optionflags & QUESTION_ADAPTIVE) and !$options->readonly) {
1158             echo '<input type="submit" name="', $question->name_prefix, 'submit" value="',
1159                     get_string('mark', 'quiz'), '" class="submit btn" />';
1160         }
1161     }
1163     /**
1164     * Return a summary of the student response
1165     *
1166     * This function returns a short string of no more than a given length that
1167     * summarizes the student's response in the given $state. This is used for
1168     * example in the response history table. This string should already be,
1169     * for output.
1170     * @return string         The summary of the student response
1171     * @param object $question
1172     * @param object $state   The state whose responses are to be summarized
1173     * @param int $length     The maximum length of the returned string
1174     */
1175     function response_summary($question, $state, $length=80) {
1176         // This should almost certainly be overridden
1177         $responses = $this->get_actual_response($question, $state);
1178         if (empty($responses) || !is_array($responses)) {
1179             $responses = array();
1180         }
1181         if (is_array($responses)) {
1182             $responses = implode(',', array_map('s', $responses));
1183         }
1184         return shorten_text($responses, $length);
1185     }
1187     /**
1188     * Renders the question for printing and returns the LaTeX source produced
1189     *
1190     * This function should render the question suitable for a printed problem
1191     * or solution sheet in LaTeX and return the rendered output.
1192     * @return string          The LaTeX output.
1193     * @param object $question The question to be rendered. Question type
1194     *                         specific information is included.
1195     * @param object $state    The state to render the question in. The
1196     *                         question type specific information is also
1197     *                         included.
1198     * @param object $cmoptions
1199     * @param string $type     Indicates if the question or the solution is to be
1200     *                         rendered with the values 'question' and
1201     *                         'solution'.
1202     */
1203     function get_texsource(&$question, &$state, $cmoptions, $type) {
1204         // The default implementation simply returns a string stating that
1205         // the question is only available online.
1207         return get_string('onlineonly', 'texsheet');
1208     }
1210     /**
1211     * Compares two question states for equivalence of the student's responses
1212     *
1213     * The responses for the two states must be examined to see if they represent
1214     * equivalent answers to the question by the student. This method will be
1215     * invoked for each of the previous states of the question before grading
1216     * occurs. If the student is found to have already attempted the question
1217     * with equivalent responses then the attempt at the question is ignored;
1218     * grading does not occur and the state does not change. Thus they are not
1219     * penalized for this case.
1220     * @return boolean
1221     * @param object $question  The question for which the states are to be
1222     *                          compared. Question type specific information is
1223     *                          included.
1224     * @param object $state     The state of the question. The responses are in
1225     *                          ->responses. This is the only field of $state
1226     *                          that it is safe to use.
1227     * @param object $teststate The state whose responses are to be
1228     *                          compared. The state will be of the same age or
1229     *                          older than $state. If possible, the method should
1230     *                          only use the field $teststate->responses, however
1231     *                          any field that is set up by restore_session_and_responses
1232     *                          can be used.
1233     */
1234     function compare_responses(&$question, $state, $teststate) {
1235         // The default implementation performs a comparison of the response
1236         // arrays. The ordering of the arrays does not matter.
1237         // Question types may wish to override this (eg. to ignore trailing
1238         // white space or to make "7.0" and "7" compare equal).
1240         // In php neither == nor === compare arrays the way you want. The following
1241         // ensures that the arrays have the same keys, with the same values.
1242         $result = false;
1243         $diff1 = array_diff_assoc($state->responses, $teststate->responses);
1244         if (empty($diff1)) {
1245             $diff2 = array_diff_assoc($teststate->responses, $state->responses);
1246             $result =  empty($diff2);
1247         }
1249         return $result;
1250     }
1252     /**
1253     * Checks whether a response matches a given answer
1254     *
1255     * This method only applies to questions that use teacher-defined answers
1256     *
1257     * @return boolean
1258     */
1259     function test_response(&$question, &$state, $answer) {
1260         $response = isset($state->responses['']) ? $state->responses[''] : '';
1261         return ($response == $answer->answer);
1262     }
1264     /**
1265     * Performs response processing and grading
1266     *
1267     * This function performs response processing and grading and updates
1268     * the state accordingly.
1269     * @return boolean         Indicates success or failure.
1270     * @param object $question The question to be graded. Question type
1271     *                         specific information is included.
1272     * @param object $state    The state of the question to grade. The current
1273     *                         responses are in ->responses. The last graded state
1274     *                         is in ->last_graded (hence the most recently graded
1275     *                         responses are in ->last_graded->responses). The
1276     *                         question type specific information is also
1277     *                         included. The ->raw_grade and ->penalty fields
1278     *                         must be updated. The method is able to
1279     *                         close the question session (preventing any further
1280     *                         attempts at this question) by setting
1281     *                         $state->event to QUESTION_EVENTCLOSEANDGRADE
1282     * @param object $cmoptions
1283     */
1284     function grade_responses(&$question, &$state, $cmoptions) {
1285         // The default implementation uses the test_response method to
1286         // compare what the student entered against each of the possible
1287         // answers stored in the question, and uses the grade from the
1288         // first one that matches. It also sets the marks and penalty.
1289         // This should be good enought for most simple question types.
1291         $state->raw_grade = 0;
1292         foreach($question->options->answers as $answer) {
1293             if($this->test_response($question, $state, $answer)) {
1294                 $state->raw_grade = $answer->fraction;
1295                 break;
1296             }
1297         }
1299         // Make sure we don't assign negative or too high marks.
1300         $state->raw_grade = min(max((float) $state->raw_grade,
1301                             0.0), 1.0) * $question->maxgrade;
1303         // Update the penalty.
1304         $state->penalty = $question->penalty * $question->maxgrade;
1306         // mark the state as graded
1307         $state->event = ($state->event ==  QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
1309         return true;
1310     }
1313     /**
1314     * Includes configuration settings for the question type on the quiz admin
1315     * page
1316     *
1317     * TODO: It makes no sense any longer to do the admin for question types
1318     * from the quiz admin page. This should be changed.
1319     * Returns an array of objects describing the options for the question type
1320     * to be included on the quiz module admin page.
1321     * Configuration options can be included by setting the following fields in
1322     * the object:
1323     * ->name           The name of the option within this question type.
1324     *                  The full option name will be constructed as
1325     *                  "quiz_{$this->name()}_$name", the human readable name
1326     *                  will be displayed with get_string($name, 'quiz').
1327     * ->code           The code to display the form element, help button, etc.
1328     *                  i.e. the content for the central table cell. Be sure
1329     *                  to name the element "quiz_{$this->name()}_$name" and
1330     *                  set the value to $CFG->{"quiz_{$this->name()}_$name"}.
1331     * ->help           Name of the string from the quiz module language file
1332     *                  to be used for the help message in the third column of
1333     *                  the table. An empty string (or the field not set)
1334     *                  means to leave the box empty.
1335     * Links to custom settings pages can be included by setting the following
1336     * fields in the object:
1337     * ->name           The name of the link text string.
1338     *                  get_string($name, 'quiz') will be called.
1339     * ->link           The filename part of the URL for the link. The full URL
1340     *                  is contructed as
1341     *                  "$CFG->wwwroot/question/type/{$this->name()}/$link?sesskey=$sesskey"
1342     *                  [but with the relavant calls to the s and rawurlencode
1343     *                  functions] where $sesskey is the sesskey for the user.
1344     * @return array    Array of objects describing the configuration options to
1345     *                  be included on the quiz module admin page.
1346     */
1347     function get_config_options() {
1348         // No options by default
1350         return false;
1351     }
1353     /**
1354     * Returns true if the editing wizard is finished, false otherwise.
1355     *
1356     * The default implementation returns true, which is suitable for all question-
1357     * types that only use one editing form. This function is used in
1358     * question.php to decide whether we can regrade any states of the edited
1359     * question and redirect to edit.php.
1360     *
1361     * The dataset dependent question-type, which is extended by the calculated
1362     * question-type, overwrites this method because it uses multiple pages (i.e.
1363     * a wizard) to set up the question and associated datasets.
1364     *
1365     * @param object $form  The data submitted by the previous page.
1366     *
1367     * @return boolean      Whether the wizard's last page was submitted or not.
1368     */
1369     function finished_edit_wizard(&$form) {
1370         //In the default case there is only one edit page.
1371         return true;
1372     }
1374     /**
1375     * Prints a table of course modules in which the question is used
1376     *
1377     * TODO: This should be made quiz-independent
1378     *
1379     * This function is used near the end of the question edit forms in all question types
1380     * It prints the table of quizzes in which the question is used
1381     * containing checkboxes to allow the teacher to replace the old question version
1382     *
1383     * @param object $question
1384     * @param object $course
1385     * @param integer $cmid optional The id of the course module currently being edited
1386     */
1387     function print_replacement_options($question, $course, $cmid='0') {
1388         global $DB;
1390         // Disable until the versioning code has been fixed
1391         if (true) {
1392             return;
1393         }
1395         // no need to display replacement options if the question is new
1396         if(empty($question->id)) {
1397             return true;
1398         }
1400         // get quizzes using the question (using the question_instances table)
1401         $quizlist = array();
1402         if(!$instances = $DB->get_records('quiz_question_instances', array('question' => $question->id))) {
1403             $instances = array();
1404         }
1405         foreach($instances as $instance) {
1406             $quizlist[$instance->quiz] = $instance->quiz;
1407         }
1408         if(empty($quizlist) or !$quizzes = $DB->get_records_list('quiz', 'id', $quizlist)) {
1409             $quizzes = array();
1410         }
1412         // do the printing
1413         if(count($quizzes) > 0) {
1414             // print the table
1415             $strquizname  = get_string('modulename', 'quiz');
1416             $strdoreplace = get_string('replace', 'quiz');
1417             $straffectedstudents = get_string('affectedstudents', 'quiz', $course->students);
1418             echo "<tr valign=\"top\">\n";
1419             echo "<td align=\"right\"><b>".get_string("replacementoptions", "quiz").":</b></td>\n";
1420             echo "<td align=\"left\">\n";
1421             echo "<table cellpadding=\"5\" align=\"left\" class=\"generalbox\" width=\"100%\">\n";
1422             echo "<tr>\n";
1423             echo "<th align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$strquizname</th>\n";
1424             echo "<th align=\"center\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$strdoreplace</th>\n";
1425             echo "<th align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$straffectedstudents</th>\n";
1426             echo "</tr>\n";
1427             foreach($quizzes as $quiz) {
1428                 // work out whethere it should be checked by default
1429                 $checked = '';
1430                 if((int)$cmid === (int)$quiz->id
1431                     or empty($quiz->usercount)) {
1432                     $checked = "checked=\"checked\"";
1433                 }
1435                 // find how many different students have already attempted this quiz
1436                 $students = array();
1437                 if($attempts = $DB->get_records_select('quiz_attempts', "quiz = ? AND preview = '0'", array($quiz->id))) {
1438                     foreach($attempts as $attempt) {
1439                         if ($DB->record_exists('question_states', array('attempt' => $attempt->uniqueid, 'question' => $question->id), 'originalquestion', 0)) {
1440                             $students[$attempt->userid] = 1;
1441                         }
1442                     }
1443                 }
1444                 $studentcount = count($students);
1446                 $strstudents = $studentcount === 1 ? $course->student : $course->students;
1447                 echo "<tr>\n";
1448                 echo "<td align=\"left\" class=\"generaltablecell c0\">".format_string($quiz->name)."</td>\n";
1449                 echo "<td align=\"center\" class=\"generaltablecell c0\"><input name=\"q{$quiz->id}replace\" type=\"checkbox\" ".$checked." /></td>\n";
1450                 echo "<td align=\"left\" class=\"generaltablecell c0\">".(($studentcount) ? $studentcount.' '.$strstudents : '-')."</td>\n";
1451                 echo "</tr>\n";
1452             }
1453             echo "</table>\n";
1454         }
1455         echo "</td></tr>\n";
1456     }
1458     /**
1459      * Call format_text from weblib.php with the options appropriate to question types.
1460      *
1461      * @param string $text the text to format.
1462      * @param integer $text the type of text. Normally $question->questiontextformat.
1463      * @param object $cmoptions the context the string is being displayed in. Only $cmoptions->course is used.
1464      * @return string the formatted text.
1465      */
1466     function format_text($text, $textformat, $cmoptions = NULL) {
1467         $formatoptions = new stdClass;
1468         $formatoptions->noclean = true;
1469         $formatoptions->para = false;
1470         return format_text($text, $textformat, $formatoptions, $cmoptions === NULL ? NULL : $cmoptions->course);
1471     }
1473     /*
1474      * Find all course / site files linked from a question.
1475      *
1476      * Need to check for links to files in question_answers.answer and feedback
1477      * and in question table in generalfeedback and questiontext fields. Methods
1478      * on child classes will also check extra question specific fields.
1479      *
1480      * Needs to be overriden for child classes that have extra fields containing
1481      * html.
1482      *
1483      * @param string html the html to search
1484      * @param int courseid search for files for courseid course or set to siteid for
1485      *              finding site files.
1486      * @return array of url, relative url is key and array with one item = question id as value
1487      *                  relative url is relative to course/site files directory root.
1488      */
1489     function find_file_links($question, $courseid){
1490         $urls = array();
1492     /// Question image
1493         if ($question->image != ''){
1494             if (substr(strtolower($question->image), 0, 7) == 'http://') {
1495                 $matches = array();
1497                 //support for older questions where we have a complete url in image field
1498                 if (preg_match('!^'.question_file_links_base_url($courseid).'(.*)!i', $question->image, $matches)){
1499                     if ($cleanedurl = question_url_check($urls[$matches[2]])){
1500                         $urls[$cleanedurl] = null;
1501                     }
1502                 }
1503             } else {
1504                 if ($question->image != ''){
1505                     if ($cleanedurl = question_url_check($question->image)){
1506                         $urls[$cleanedurl] = null;//will be set later
1507                     }
1508                 }
1510             }
1512         }
1514     /// Questiontext and general feedback.
1515         $urls += question_find_file_links_from_html($question->questiontext, $courseid);
1516         $urls += question_find_file_links_from_html($question->generalfeedback, $courseid);
1518     /// Answers, if this question uses them.
1519         if (isset($question->options->answers)){
1520             foreach ($question->options->answers as $answerkey => $answer){
1521             /// URLs in the answers themselves, if appropriate.
1522                 if ($this->has_html_answers()) {
1523                     $urls += question_find_file_links_from_html($answer->answer, $courseid);
1524                 }
1525             /// URLs in the answer feedback.
1526                 $urls += question_find_file_links_from_html($answer->feedback, $courseid);
1527             }
1528         }
1530     /// Set all the values of the array to the question object
1531         if ($urls){
1532             $urls = array_combine(array_keys($urls), array_fill(0, count($urls), array($question->id)));
1533         }
1534         return $urls;
1535     }
1536     /*
1537      * Find all course / site files linked from a question.
1538      *
1539      * Need to check for links to files in question_answers.answer and feedback
1540      * and in question table in generalfeedback and questiontext fields. Methods
1541      * on child classes will also check extra question specific fields.
1542      *
1543      * Needs to be overriden for child classes that have extra fields containing
1544      * html.
1545      *
1546      * @param string html the html to search
1547      * @param int course search for files for courseid course or set to siteid for
1548      *              finding site files.
1549      * @return array of files, file name is key and array with one item = question id as value
1550      */
1551     function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){
1552         global $CFG, $DB;
1553         $updateqrec = false;
1555     /// Question image
1556         if (!empty($question->image)){
1557             //support for older questions where we have a complete url in image field
1558             if (substr(strtolower($question->image), 0, 7) == 'http://') {
1559                 $questionimage = preg_replace('!^'.question_file_links_base_url($fromcourseid).preg_quote($url, '!').'$!i', $destination, $question->image, 1);
1560             } else {
1561                 $questionimage = preg_replace('!^'.preg_quote($url, '!').'$!i', $destination, $question->image, 1);
1562             }
1563             if ($questionimage != $question->image){
1564                 $question->image = $questionimage;
1565                 $updateqrec = true;
1566             }
1567         }
1569     /// Questiontext and general feedback.
1570         $question->questiontext = question_replace_file_links_in_html($question->questiontext, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
1571         $question->generalfeedback = question_replace_file_links_in_html($question->generalfeedback, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
1573     /// If anything has changed, update it in the database.
1574         if ($updateqrec){
1575             if (!$DB->update_record('question', $question)){
1576                 error ('Couldn\'t update question '.$question->name);
1577             }
1578         }
1581     /// Answers, if this question uses them.
1582         if (isset($question->options->answers)){
1583             //answers that do not need updating have been unset
1584             foreach ($question->options->answers as $answer){
1585                 $answerchanged = false;
1586             /// URLs in the answers themselves, if appropriate.
1587                 if ($this->has_html_answers()) {
1588                     $answer->answer = question_replace_file_links_in_html($answer->answer, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
1589                 }
1590             /// URLs in the answer feedback.
1591                 $answer->feedback = question_replace_file_links_in_html($answer->feedback, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
1592             /// If anything has changed, update it in the database.
1593                 if ($answerchanged){
1594                     if (!$DB->update_record('question_answers', $answer)){
1595                         error ('Couldn\'t update question ('.$question->name.') answer '.$answer->id);
1596                     }
1597                 }
1598             }
1599         }
1600     }
1601     /**
1602      * @return the best link to pass to print_error.
1603      * @param $cmoptions as passed in from outside.
1604      */
1605     function error_link($cmoptions) {
1606         global $CFG;
1607         $cm = get_coursemodule_from_instance('quiz', $cmoptions->id);
1608         if (!empty($cm->id)) {
1609             return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
1610         } else if (!empty($cm->course)) {
1611             return $CFG->wwwroot . '/course/view.php?id=' . $cm->course;
1612         } else {
1613             return '';
1614         }
1615     }
1617 /// BACKUP FUNCTIONS ////////////////////////////
1619     /*
1620      * Backup the data in the question
1621      *
1622      * This is used in question/backuplib.php
1623      */
1624     function backup($bf,$preferences,$question,$level=6) {
1625         // The default type has nothing to back up
1626         return true;
1627     }
1629 /// RESTORE FUNCTIONS /////////////////
1631     /*
1632      * Restores the data in the question
1633      *
1634      * This is used in question/restorelib.php
1635      */
1636     function restore($old_question_id,$new_question_id,$info,$restore) {
1637         // The default question type has nothing to restore
1638         return true;
1639     }
1641     function restore_map($old_question_id,$new_question_id,$info,$restore) {
1642         // There is nothing to decode
1643         return true;
1644     }
1646     function restore_recode_answer($state, $restore) {
1647         // There is nothing to decode
1648         return $state->answer;
1649     }
1651     /**
1652      * Abstract function implemented by each question type. It runs all the code
1653      * required to set up and save a question of any type for testing purposes.
1654      * Alternate DB table prefix may be used to facilitate data deletion.
1655      */
1656     function generate_test($name, $courseid=null) {
1657         $form = new stdClass();
1658         $form->name = $name;
1659         $form->questiontextformat = 1;
1660         $form->questiontext = 'test question, generated by script';
1661         $form->defaultgrade = 1;
1662         $form->penalty = 0.1;
1663         $form->generalfeedback = "Well done";
1665         $context = get_context_instance(CONTEXT_COURSE, $courseid);
1666         $newcategory = question_make_default_categories(array($context));
1667         $form->category = $newcategory->id . ',1';
1669         $question = new stdClass();
1670         $question->courseid = $courseid;
1671         $question->qtype = $this->qtype;
1672         return array($form, $question);
1673     }
1675 ?>