Merge commit 'catalyst/MOODLE_19_STABLE' into mdl19-linuxchix
[moodle-linuxchix.git] / question / type / questiontype.php
blob912695697876158136b45de10f05859679cda4b8
1 <?php // $Id$
2 /**
3 * The default questiontype class.
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
14 require_once($CFG->libdir . '/questionlib.php');
16 /**
17 * This is the base class for Moodle question types.
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.
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.
29 * @package questionbank
30 * @subpackage questiontypes
32 class default_questiontype {
34 /**
35 * Name of the question type
37 * The name returned should coincide with the name of the directory
38 * in which this questiontype is located
40 * @return string the name of this question type.
42 function name() {
43 return 'default';
46 /**
47 * The name this question should appear as in the create new question
48 * dropdown.
50 * @return mixed the desired string, or false to hide this question type in the menu.
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');
60 return $menu_name;
63 /**
64 * @return boolean true if this question can only be graded manually.
66 function is_manual_graded() {
67 return false;
70 /**
71 * @return boolean true if this question type can be used by the random question type.
73 function is_usable_by_random() {
74 return true;
77 /**
78 * @return whether the question_answers.answer field needs to have
79 * restore_decode_content_links_worker called on it.
81 function has_html_answers() {
82 return false;
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).
91 * @return mixed array as above, or null to tell the base class to do nothing.
93 function extra_question_fields() {
94 return null;
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).
102 * @return mixed array as above, or null to tell the base class to do nothing.
104 function extra_answer_fields() {
105 return null;
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.
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.
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;
124 require_once($definition_file);
125 $classname = 'question_edit_'.$this->name().'_form';
126 if (!class_exists($classname)) {
127 return null;
129 return new $classname($submiturl, $question, $category, $contexts, $formeditable);
133 * @return string the full path of the folder this plugin's files live in.
135 function plugin_dir() {
136 global $CFG;
137 return $CFG->dirroot . '/question/type/' . $this->name();
141 * @return string the URL of the folder this plugin's files live in.
143 function plugin_baseurl() {
144 global $CFG;
145 return $CFG->wwwroot . '/question/type/' . $this->name();
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.
152 * @param question_edit_form $mform a child of question_edit_form
153 * @param object $question
154 * @param string $wizardnow is '' for first page.
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');
164 if ($question->formoptions->canmove){
165 $permissionstrs[] = get_string('permissionmove', 'question');
167 if ($question->formoptions->cansaveasnew){
168 $permissionstrs[] = get_string('permissionsaveasnew', 'question');
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>';
177 $html .= '</ul>';
178 print_box($html, 'boxwidthnarrow boxaligncenter generalbox');
180 $mform->display();
184 * Method called by display_question_editing_page and by question.php to get heading for breadcrumbs.
186 * @return array a string heading and the langmodule in which it was found.
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;
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);
203 return array($strheading, $langmodule);
209 * @param $question
211 function set_default_options(&$question) {
215 * Saves or updates a question after editing by a teacher
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.
232 function save_question($question, $form, $course) {
233 global $USER;
235 // This default implementation is suitable for most
236 // question types.
238 // First, save the basic question itself
239 $question->name = trim($form->name);
240 $question->questiontext = trim($form->questiontext);
241 $question->questiontextformat = $form->questiontextformat;
242 $question->parent = isset($form->parent)? $form->parent : 0;
243 $question->length = $this->actual_number_of_questions($question);
244 $question->penalty = isset($form->penalty) ? $form->penalty : 0;
246 if (empty($form->image)) {
247 $question->image = "";
248 } else {
249 $question->image = $form->image;
252 if (empty($form->generalfeedback)) {
253 $question->generalfeedback = '';
254 } else {
255 $question->generalfeedback = trim($form->generalfeedback);
258 if (empty($question->name)) {
259 $question->name = shorten_text(strip_tags($question->questiontext), 15);
260 if (empty($question->name)) {
261 $question->name = '-';
265 if ($question->penalty > 1 or $question->penalty < 0) {
266 $question->errors['penalty'] = get_string('invalidpenalty', 'quiz');
269 if (isset($form->defaultgrade)) {
270 $question->defaultgrade = $form->defaultgrade;
273 if (!empty($question->id)) { // Question already exists
274 if (isset($form->categorymoveto)){
275 question_require_capability_on($question, 'move');
276 list($question->category, $movetocontextid) = explode(',', $form->categorymoveto);
277 //don't need to test add permission of category we are moving question to.
278 //Only categories that we have permission to add
279 //a question to will get through the form cleaning code for the select box.
281 // keep existing unique stamp code
282 $question->stamp = get_field('question', 'stamp', 'id', $question->id);
283 $question->modifiedby = $USER->id;
284 $question->timemodified = time();
285 if (!update_record('question', $question)) {
286 error('Could not update question!');
288 } else { // Question is a new one
289 if (isset($form->categorymoveto)){
290 // Doing save as new question, and we have move rights.
291 list($question->category, $notused) = explode(',', $form->categorymoveto);
292 //don't need to test add permission of category we are moving question to.
293 //Only categories that we have permission to add
294 //a question to will get through the form cleaning code for the select box.
295 } else {
296 // Really a new question.
297 list($question->category, $notused) = explode(',', $form->category);
299 // Set the unique code
300 $question->stamp = make_unique_id_code();
301 $question->createdby = $USER->id;
302 $question->modifiedby = $USER->id;
303 $question->timecreated = time();
304 $question->timemodified = time();
305 if (!$question->id = insert_record('question', $question)) {
306 print_object($question);
307 error('Could not insert new question!');
311 // Now to save all the answers and type-specific options
313 $form->id = $question->id;
314 $form->qtype = $question->qtype;
315 $form->category = $question->category;
316 $form->questiontext = $question->questiontext;
318 $result = $this->save_question_options($form);
320 if (!empty($result->error)) {
321 error($result->error);
324 if (!empty($result->notice)) {
325 notice($result->notice, "question.php?id=$question->id");
328 if (!empty($result->noticeyesno)) {
329 notice_yesno($result->noticeyesno, "question.php?id=$question->id&amp;courseid={$course->id}",
330 "edit.php?courseid={$course->id}");
331 print_footer($course);
332 exit;
335 // Give the question a unique version stamp determined by question_hash()
336 if (!set_field('question', 'version', question_hash($question), 'id', $question->id)) {
337 error('Could not update question version field');
340 return $question;
344 * Saves question-type specific options
346 * This is called by {@link save_question()} to save the question-type specific data
347 * @return object $result->error or $result->noticeyesno or $result->notice
348 * @param object $question This holds the information from the editing form,
349 * it is not a standard question object.
351 function save_question_options($question) {
352 $extra_question_fields = $this->extra_question_fields();
354 if (is_array($extra_question_fields)) {
355 $question_extension_table = array_shift($extra_question_fields);
357 $function = 'update_record';
358 $options = get_record($question_extension_table, 'questionid', $question->id);
359 if (!$options) {
360 $function = 'insert_record';
361 $options = new stdClass;
362 $options->questionid = $question->id;
364 foreach ($extra_question_fields as $field) {
365 if (!isset($question->$field)) {
366 $result = new stdClass;
367 $result->error = "No data for field $field when saving " .
368 $this->name() . ' question id ' . $question->id;
369 return $result;
371 $options->$field = $question->$field;
374 if (!$function($question_extension_table, $options)) {
375 $result = new stdClass;
376 $result->error = 'Could not save question options for ' .
377 $this->name() . ' question id ' . $question->id;
378 return $result;
382 $extra_answer_fields = $this->extra_answer_fields();
383 // TODO save the answers, with any extra data.
385 return null;
389 * Changes all states for the given attempts over to a new question
391 * This is used by the versioning code if the teacher requests that a question
392 * gets replaced by the new version. In order for the attempts to be regraded
393 * properly all data in the states referring to the old question need to be
394 * changed to refer to the new version instead. In particular for question types
395 * that use the answers table the answers belonging to the old question have to
396 * be changed to those belonging to the new version.
398 * @param integer $oldquestionid The id of the old question
399 * @param object $newquestion The new question
400 * @param array $attempts An array of all attempt objects in whose states
401 * replacement should take place
403 function replace_question_in_attempts($oldquestionid, $newquestion, $attemtps) {
404 echo 'Not yet implemented';
405 return;
409 * Loads the question type specific options for the question.
411 * This function loads any question type specific options for the
412 * question from the database into the question object. This information
413 * is placed in the $question->options field. A question type is
414 * free, however, to decide on a internal structure of the options field.
415 * @return bool Indicates success or failure.
416 * @param object $question The question object for the question. This object
417 * should be updated to include the question type
418 * specific information (it is passed by reference).
420 function get_question_options(&$question) {
421 global $CFG;
423 if (!isset($question->options)) {
424 $question->options = new object;
427 $extra_question_fields = $this->extra_question_fields();
428 if (is_array($extra_question_fields)) {
429 $question_extension_table = array_shift($extra_question_fields);
430 $extra_data = get_record($question_extension_table, 'questionid', $question->id, '', '', '', '', implode(', ', $extra_question_fields));
431 if ($extra_data) {
432 foreach ($extra_question_fields as $field) {
433 $question->options->$field = $extra_data->$field;
435 } else {
436 notify("Failed to load question options from the table $question_extension_table for questionid " .
437 $question->id);
438 return false;
442 $extra_answer_fields = $this->extra_answer_fields();
443 if (is_array($extra_answer_fields)) {
444 $answer_extension_table = array_shift($extra_answer_fields);
445 $question->options->answers = get_records_sql('
446 SELECT qa.*, qax.' . implode(', qax.', $extra_answer_fields) . '
447 FROM ' . $CFG->prefix . 'question_answers qa, ' . $CFG->prefix . '$answer_extension_table qax
448 WHERE qa.questionid = ' . $question->id . ' AND qax.answerid = qa.id');
449 if (!$question->options->answers) {
450 notify("Failed to load question answers from the table $answer_extension_table for questionid " .
451 $question->id);
452 return false;
454 } else {
455 // Don't check for success or failure because some question types do not use the answers table.
456 $question->options->answers = get_records('question_answers', 'question', $question->id, 'id ASC');
459 return true;
463 * Deletes states from the question-type specific tables
465 * @param string $stateslist Comma separated list of state ids to be deleted
467 function delete_states($stateslist) {
468 /// The default question type does not have any tables of its own
469 // therefore there is nothing to delete
471 return true;
475 * Deletes a question from the question-type specific tables
477 * @return boolean Success/Failure
478 * @param object $question The question being deleted
480 function delete_question($questionid) {
481 global $CFG;
482 $success = true;
484 $extra_question_fields = $this->extra_question_fields();
485 if (is_array($extra_question_fields)) {
486 $question_extension_table = array_shift($extra_question_fields);
487 $success = $success && delete_records($question_extension_table, 'questionid', $questionid);
490 $extra_answer_fields = $this->extra_answer_fields();
491 if (is_array($extra_answer_fields)) {
492 $answer_extension_table = array_shift($extra_answer_fields);
493 $success = $success && delete_records_select($answer_extension_table,
494 "answerid IN (SELECT qa.id FROM {$CFG->prefix}question_answers qa WHERE qa.question = $questionid)");
497 $success = $success && delete_records('question_answers', 'question', $questionid);
499 return $success;
503 * Returns the number of question numbers which are used by the question
505 * This function returns the number of question numbers to be assigned
506 * to the question. Most question types will have length one; they will be
507 * assigned one number. The 'description' type, however does not use up a
508 * number and so has a length of zero. Other question types may wish to
509 * handle a bundle of questions and hence return a number greater than one.
510 * @return integer The number of question numbers which should be
511 * assigned to the question.
512 * @param object $question The question whose length is to be determined.
513 * Question type specific information is included.
515 function actual_number_of_questions($question) {
516 // By default, each question is given one number
517 return 1;
521 * Creates empty session and response information for the question
523 * This function is called to start a question session. Empty question type
524 * specific session data (if any) and empty response data will be added to the
525 * state object. Session data is any data which must persist throughout the
526 * attempt possibly with updates as the user interacts with the
527 * question. This function does NOT create new entries in the database for
528 * the session; a call to the {@link save_session_and_responses} member will
529 * occur to do this.
530 * @return bool Indicates success or failure.
531 * @param object $question The question for which the session is to be
532 * created. Question type specific information is
533 * included.
534 * @param object $state The state to create the session for. Note that
535 * this will not have been saved in the database so
536 * there will be no id. This object will be updated
537 * to include the question type specific information
538 * (it is passed by reference). In particular, empty
539 * responses will be created in the ->responses
540 * field.
541 * @param object $cmoptions
542 * @param object $attempt The attempt for which the session is to be
543 * started. Questions may wish to initialize the
544 * session in different ways depending on the user id
545 * or time available for the attempt.
547 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
548 // The default implementation should work for the legacy question types.
549 // Most question types with only a single form field for the student's response
550 // will use the empty string '' as the index for that one response. This will
551 // automatically be stored in and restored from the answer field in the
552 // question_states table.
553 $state->responses = array(
554 '' => '',
556 return true;
560 * Restores the session data and most recent responses for the given state
562 * This function loads any session data associated with the question
563 * session in the given state from the database into the state object.
564 * In particular it loads the responses that have been saved for the given
565 * state into the ->responses member of the state object.
567 * Question types with only a single form field for the student's response
568 * will not need not restore the responses; the value of the answer
569 * field in the question_states table is restored to ->responses['']
570 * before this function is called. Question types with more response fields
571 * should override this method and set the ->responses field to an
572 * associative array of responses.
573 * @return bool Indicates success or failure.
574 * @param object $question The question object for the question including any
575 * question type specific information.
576 * @param object $state The saved state to load the session for. This
577 * object should be updated to include the question
578 * type specific session information and responses
579 * (it is passed by reference).
581 function restore_session_and_responses(&$question, &$state) {
582 // The default implementation does nothing (successfully)
583 return true;
587 * Saves the session data and responses for the given question and state
589 * This function saves the question type specific session data from the
590 * state object to the database. In particular for most question types it saves the
591 * responses from the ->responses member of the state object. The question type
592 * non-specific data for the state has already been saved in the question_states
593 * table and the state object contains the corresponding id and
594 * sequence number which may be used to index a question type specific table.
596 * Question types with only a single form field for the student's response
597 * which is contained in ->responses[''] will not have to save this response,
598 * it will already have been saved to the answer field of the question_states table.
599 * Question types with more response fields should override this method to convert
600 * the data the ->responses array into a single string field, and save it in the
601 * database. The implementation in the multichoice question type is a good model to follow.
602 * http://cvs.moodle.org/contrib/plugins/question/type/opaque/questiontype.php?view=markup
603 * has a solution that is probably quite generally applicable.
604 * @return bool Indicates success or failure.
605 * @param object $question The question object for the question including
606 * the question type specific information.
607 * @param object $state The state for which the question type specific
608 * data and responses should be saved.
610 function save_session_and_responses(&$question, &$state) {
611 // The default implementation does nothing (successfully)
612 return true;
616 * Returns an array of values which will give full marks if graded as
617 * the $state->responses field
619 * The correct answer to the question in the given state, or an example of
620 * a correct answer if there are many, is returned. This is used by some question
621 * types in the {@link grade_responses()} function but it is also used by the
622 * question preview screen to fill in correct responses.
623 * @return mixed A response array giving the responses corresponding
624 * to the (or a) correct answer to the question. If there is
625 * no correct answer that scores 100% then null is returned.
626 * @param object $question The question for which the correct answer is to
627 * be retrieved. Question type specific information is
628 * available.
629 * @param object $state The state of the question, for which a correct answer is
630 * needed. Question type specific information is included.
632 function get_correct_responses(&$question, &$state) {
633 /* The default implementation returns the response for the first answer
634 that gives full marks. */
635 if ($question->options->answers) {
636 foreach ($question->options->answers as $answer) {
637 if (((int) $answer->fraction) === 1) {
638 return array('' => addslashes($answer->answer));
642 return null;
646 * Return an array of values with the texts for all possible responses stored
647 * for the question
649 * All answers are found and their text values isolated
650 * @return object A mixed object
651 * ->id question id. Needed to manage random questions:
652 * it's the id of the actual question presented to user in a given attempt
653 * ->responses An array of values giving the responses corresponding
654 * to all answers to the question. Answer ids are used as keys.
655 * The text and partial credit are the object components
656 * @param object $question The question for which the answers are to
657 * be retrieved. Question type specific information is
658 * available.
660 // ULPGC ecastro
661 function get_all_responses(&$question, &$state) {
662 if (isset($question->options->answers) && is_array($question->options->answers)) {
663 $answers = array();
664 foreach ($question->options->answers as $aid=>$answer) {
665 $r = new stdClass;
666 $r->answer = $answer->answer;
667 $r->credit = $answer->fraction;
668 $answers[$aid] = $r;
670 $result = new stdClass;
671 $result->id = $question->id;
672 $result->responses = $answers;
673 return $result;
674 } else {
675 return null;
680 * Return the actual response to the question in a given state
681 * for the question.
683 * @return mixed An array containing the response or reponses (multiple answer, match)
684 * given by the user in a particular attempt.
685 * @param object $question The question for which the correct answer is to
686 * be retrieved. Question type specific information is
687 * available.
688 * @param object $state The state object that corresponds to the question,
689 * for which a correct answer is needed. Question
690 * type specific information is included.
692 // ULPGC ecastro
693 function get_actual_response($question, $state) {
694 if (!empty($state->responses)) {
695 $responses[] = stripslashes($state->responses['']);
696 } else {
697 $responses[] = '';
699 return $responses;
702 // ULPGC ecastro
703 function get_fractional_grade(&$question, &$state) {
704 $maxgrade = $question->maxgrade;
705 $grade = $state->grade;
706 if ($maxgrade) {
707 return (float)($grade/$maxgrade);
708 } else {
709 return (float)$grade;
715 * Checks if the response given is correct and returns the id
717 * @return int The ide number for the stored answer that matches the response
718 * given by the user in a particular attempt.
719 * @param object $question The question for which the correct answer is to
720 * be retrieved. Question type specific information is
721 * available.
722 * @param object $state The state object that corresponds to the question,
723 * for which a correct answer is needed. Question
724 * type specific information is included.
726 // ULPGC ecastro
727 function check_response(&$question, &$state){
728 return false;
731 // Used by the following function, so that it only returns results once per quiz page.
732 var $already_done = false;
734 * If this question type requires extra CSS or JavaScript to function,
735 * then this method will return an array of <link ...> tags that reference
736 * those stylesheets. This function will also call require_js()
737 * from ajaxlib.php, to get any necessary JavaScript linked in too.
739 * The two parameters match the first two parameters of print_question.
741 * @param object $question The question object.
742 * @param object $state The state object.
744 * @return an array of bits of HTML to add to the head of pages where
745 * this question is print_question-ed in the body. The array should use
746 * integer array keys, which have no significance.
748 function get_html_head_contributions(&$question, &$state) {
749 // By default, we link to any of the files styles.css, styles.php,
750 // script.js or script.php that exist in the plugin folder.
751 // Core question types should not use this mechanism. Their styles
752 // should be included in the standard theme.
754 // We only do this once
755 // for this question type, no matter how often this method is called.
756 if ($this->already_done) {
757 return array();
759 $this->already_done = true;
761 $plugindir = $this->plugin_dir();
762 $baseurl = $this->plugin_baseurl();
763 $stylesheets = array();
764 if (file_exists($plugindir . '/styles.css')) {
765 $stylesheets[] = 'styles.css';
767 if (file_exists($plugindir . '/styles.php')) {
768 $stylesheets[] = 'styles.php';
770 if (file_exists($plugindir . '/script.js')) {
771 require_js($baseurl . '/script.js');
773 if (file_exists($plugindir . '/script.php')) {
774 require_js($baseurl . '/script.php');
776 $contributions = array();
777 foreach ($stylesheets as $stylesheet) {
778 $contributions[] = '<link rel="stylesheet" type="text/css" href="' .
779 $baseurl . '/' . $stylesheet . '" />';
781 return $contributions;
785 * Prints the question including the number, grading details, content,
786 * feedback and interactions
788 * This function prints the question including the question number,
789 * grading details, content for the question, any feedback for the previously
790 * submitted responses and the interactions. The default implementation calls
791 * various other methods to print each of these parts and most question types
792 * will just override those methods.
793 * @param object $question The question to be rendered. Question type
794 * specific information is included. The
795 * maximum possible grade is in ->maxgrade. The name
796 * prefix for any named elements is in ->name_prefix.
797 * @param object $state The state to render the question in. The grading
798 * information is in ->grade, ->raw_grade and
799 * ->penalty. The current responses are in
800 * ->responses. This is an associative array (or the
801 * empty string or null in the case of no responses
802 * submitted). The last graded state is in
803 * ->last_graded (hence the most recently graded
804 * responses are in ->last_graded->responses). The
805 * question type specific information is also
806 * included.
807 * @param integer $number The number for this question.
808 * @param object $cmoptions
809 * @param object $options An object describing the rendering options.
811 function print_question(&$question, &$state, $number, $cmoptions, $options) {
812 /* The default implementation should work for most question types
813 provided the member functions it calls are overridden where required.
814 The layout is determined by the template question.html */
816 global $CFG;
817 $isgraded = question_state_is_graded($state->last_graded);
819 // get the context so we can determine whether some extra links
820 // should be shown.
821 if (!empty($cmoptions->id)) {
822 $cm = get_coursemodule_from_instance('quiz', $cmoptions->id);
823 $context = get_context_instance(CONTEXT_MODULE, $cm->id);
824 $cmorcourseid = '&amp;cmid='.$cm->id;
825 } else if (!empty($cmoptions->course)) {
826 $context = get_context_instance(CONTEXT_COURSE, $cmoptions->course);
827 $cmorcourseid = '&amp;courseid='.$cmoptions->course;
828 } else {
829 error('Need to provide courseid or cmid to print_question.');
832 // For editing teachers print a link to an editing popup window
833 $editlink = '';
834 if (question_has_capability_on($question, 'edit')) {
835 $stredit = get_string('edit');
836 $linktext = '<img src="'.$CFG->pixpath.'/t/edit.gif" alt="'.$stredit.'" />';
837 $editlink = link_to_popup_window('/question/question.php?inpopup=1&amp;id='.$question->id.$cmorcourseid,
838 'editquestion', $linktext, 450, 550, $stredit, '', true);
841 $generalfeedback = '';
842 if ($isgraded && $options->generalfeedback) {
843 $generalfeedback = $this->format_text($question->generalfeedback,
844 $question->questiontextformat, $cmoptions);
847 $grade = '';
848 if ($question->maxgrade and $options->scores) {
849 if ($cmoptions->optionflags & QUESTION_ADAPTIVE) {
850 $grade = !$isgraded ? '--/' : round($state->last_graded->grade, $cmoptions->decimalpoints).'/';
852 $grade .= $question->maxgrade;
855 $comment = stripslashes($state->manualcomment);
856 $commentlink = '';
858 if (isset($options->questioncommentlink) && $context && has_capability('mod/quiz:grade', $context)) {
859 $strcomment = get_string('commentorgrade', 'quiz');
860 $question_to_comment = isset($question->randomquestionid) ? $question->randomquestionid : $question->id;
861 $commentlink = '<div class="commentlink">'.link_to_popup_window ($options->questioncommentlink.'?attempt='.$state->attempt.'&amp;question='.$question_to_comment,
862 'commentquestion', $strcomment, 450, 650, $strcomment, 'none', true).'</div>';
865 $history = $this->history($question, $state, $number, $cmoptions, $options);
867 include "$CFG->dirroot/question/type/question.html";
871 * Print history of responses
873 * Used by print_question()
875 function history($question, $state, $number, $cmoptions, $options) {
876 $history = '';
877 if(isset($options->history) and $options->history) {
878 if ($options->history == 'all') {
879 // show all states
880 $states = get_records_select('question_states', "attempt = '$state->attempt' AND question = '$question->id' AND event > '0'", 'seq_number ASC');
881 } else {
882 // show only graded states
883 $states = get_records_select('question_states', "attempt = '$state->attempt' AND question = '$question->id' AND event IN (".QUESTION_EVENTS_GRADED.")", 'seq_number ASC');
885 if (count($states) > 1) {
886 $strreviewquestion = get_string('reviewresponse', 'quiz');
887 $table = new stdClass;
888 $table->width = '100%';
889 if ($options->scores) {
890 $table->head = array (
891 get_string('numberabbr', 'quiz'),
892 get_string('action', 'quiz'),
893 get_string('response', 'quiz'),
894 get_string('time'),
895 get_string('score', 'quiz'),
896 //get_string('penalty', 'quiz'),
897 get_string('grade', 'quiz'),
899 } else {
900 $table->head = array (
901 get_string('numberabbr', 'quiz'),
902 get_string('action', 'quiz'),
903 get_string('response', 'quiz'),
904 get_string('time'),
908 foreach ($states as $st) {
909 $st->responses[''] = $st->answer;
910 $this->restore_session_and_responses($question, $st);
911 $b = ($state->id == $st->id) ? '<b>' : '';
912 $be = ($state->id == $st->id) ? '</b>' : '';
913 if ($state->id == $st->id) {
914 $link = '<b>'.$st->seq_number.'</b>';
915 } else {
916 if(isset($options->questionreviewlink)) {
917 $link = link_to_popup_window ($options->questionreviewlink.'?state='.$st->id.'&amp;number='.$number,
918 'reviewquestion', $st->seq_number, 450, 650, $strreviewquestion, 'none', true);
919 } else {
920 $link = $st->seq_number;
923 if ($options->scores) {
924 $table->data[] = array (
925 $link,
926 $b.get_string('event'.$st->event, 'quiz').$be,
927 $b.$this->response_summary($question, $st).$be,
928 $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be,
929 $b.round($st->raw_grade, $cmoptions->decimalpoints).$be,
930 //$b.round($st->penalty, $cmoptions->decimalpoints).$be,
931 $b.round($st->grade, $cmoptions->decimalpoints).$be
933 } else {
934 $table->data[] = array (
935 $link,
936 $b.get_string('event'.$st->event, 'quiz').$be,
937 $b.$this->response_summary($question, $st).$be,
938 $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be,
942 $history = print_table($table, true);
945 return $history;
950 * Prints the score obtained and maximum score available plus any penalty
951 * information
953 * This function prints a summary of the scoring in the most recently
954 * graded state (the question may not have been submitted for marking at
955 * the current state). The default implementation should be suitable for most
956 * question types.
957 * @param object $question The question for which the grading details are
958 * to be rendered. Question type specific information
959 * is included. The maximum possible grade is in
960 * ->maxgrade.
961 * @param object $state The state. In particular the grading information
962 * is in ->grade, ->raw_grade and ->penalty.
963 * @param object $cmoptions
964 * @param object $options An object describing the rendering options.
966 function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
967 /* The default implementation prints the number of marks if no attempt
968 has been made. Otherwise it displays the grade obtained out of the
969 maximum grade available and a warning if a penalty was applied for the
970 attempt and displays the overall grade obtained counting all previous
971 responses (and penalties) */
973 if (QUESTION_EVENTDUPLICATE == $state->event) {
974 echo ' ';
975 print_string('duplicateresponse', 'quiz');
977 if (!empty($question->maxgrade) && $options->scores) {
978 if (question_state_is_graded($state->last_graded)) {
979 // Display the grading details from the last graded state
980 $grade = new stdClass;
981 $grade->cur = round($state->last_graded->grade, $cmoptions->decimalpoints);
982 $grade->max = $question->maxgrade;
983 $grade->raw = round($state->last_graded->raw_grade, $cmoptions->decimalpoints);
985 // let student know wether the answer was correct
986 echo '<div class="correctness ';
987 if ($state->last_graded->raw_grade >= $question->maxgrade/1.01) { // We divide by 1.01 so that rounding errors dont matter.
988 echo ' correct">';
989 print_string('correct', 'quiz');
990 } else if ($state->last_graded->raw_grade > 0) {
991 echo ' partiallycorrect">';
992 print_string('partiallycorrect', 'quiz');
993 } else {
994 echo ' incorrect">';
995 print_string('incorrect', 'quiz');
997 echo '</div>';
999 echo '<div class="gradingdetails">';
1000 // print grade for this submission
1001 print_string('gradingdetails', 'quiz', $grade);
1002 if ($cmoptions->penaltyscheme) {
1003 // print details of grade adjustment due to penalties
1004 if ($state->last_graded->raw_grade > $state->last_graded->grade){
1005 echo ' ';
1006 print_string('gradingdetailsadjustment', 'quiz', $grade);
1008 // print info about new penalty
1009 // penalty is relevant only if the answer is not correct and further attempts are possible
1010 if (($state->last_graded->raw_grade < $question->maxgrade / 1.01)
1011 and (QUESTION_EVENTCLOSEANDGRADE !== $state->event)) {
1013 if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) {
1014 // A penalty was applied so display it
1015 echo ' ';
1016 print_string('gradingdetailspenalty', 'quiz', $state->last_graded->penalty);
1017 } else {
1018 /* No penalty was applied even though the answer was
1019 not correct (eg. a syntax error) so tell the student
1020 that they were not penalised for the attempt */
1021 echo ' ';
1022 print_string('gradingdetailszeropenalty', 'quiz');
1026 echo '</div>';
1032 * Prints the main content of the question including any interactions
1034 * This function prints the main content of the question including the
1035 * interactions for the question in the state given. The last graded responses
1036 * are printed or indicated and the current responses are selected or filled in.
1037 * Any names (eg. for any form elements) are prefixed with $question->name_prefix.
1038 * This method is called from the print_question method.
1039 * @param object $question The question to be rendered. Question type
1040 * specific information is included. The name
1041 * prefix for any named elements is in ->name_prefix.
1042 * @param object $state The state to render the question in. The grading
1043 * information is in ->grade, ->raw_grade and
1044 * ->penalty. The current responses are in
1045 * ->responses. This is an associative array (or the
1046 * empty string or null in the case of no responses
1047 * submitted). The last graded state is in
1048 * ->last_graded (hence the most recently graded
1049 * responses are in ->last_graded->responses). The
1050 * question type specific information is also
1051 * included.
1052 * The state is passed by reference because some adaptive
1053 * questions may want to update it during rendering
1054 * @param object $cmoptions
1055 * @param object $options An object describing the rendering options.
1057 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
1058 /* This default implementation prints an error and must be overridden
1059 by all question type implementations, unless the default implementation
1060 of print_question has been overridden. */
1062 notify('Error: Question formulation and input controls has not'
1063 .' been implemented for question type '.$this->name());
1067 * Prints the submit button(s) for the question in the given state
1069 * This function prints the submit button(s) for the question in the
1070 * given state. The name of any button created will be prefixed with the
1071 * unique prefix for the question in $question->name_prefix. The suffix
1072 * 'submit' is reserved for the single question submit button and the suffix
1073 * 'validate' is reserved for the single question validate button (for
1074 * question types which support it). Other suffixes will result in a response
1075 * of that name in $state->responses which the printing and grading methods
1076 * can then use.
1077 * @param object $question The question for which the submit button(s) are to
1078 * be rendered. Question type specific information is
1079 * included. The name prefix for any
1080 * named elements is in ->name_prefix.
1081 * @param object $state The state to render the buttons for. The
1082 * question type specific information is also
1083 * included.
1084 * @param object $cmoptions
1085 * @param object $options An object describing the rendering options.
1087 function print_question_submit_buttons(&$question, &$state, $cmoptions, $options) {
1088 /* The default implementation should be suitable for most question
1089 types. It prints a mark button in the case where individual marking is
1090 allowed. */
1092 if (($cmoptions->optionflags & QUESTION_ADAPTIVE) and !$options->readonly) {
1093 echo '<input type="submit" name="', $question->name_prefix, 'submit" value="',
1094 get_string('mark', 'quiz'), '" class="submit btn" onclick="',
1095 "form.action = form.action + '#q", $question->id, "'; return true;", '" />';
1100 * Return a summary of the student response
1102 * This function returns a short string of no more than a given length that
1103 * summarizes the student's response in the given $state. This is used for
1104 * example in the response history table. This string should already be,
1105 * for output.
1106 * @return string The summary of the student response
1107 * @param object $question
1108 * @param object $state The state whose responses are to be summarized
1109 * @param int $length The maximum length of the returned string
1111 function response_summary($question, $state, $length=80) {
1112 // This should almost certainly be overridden
1113 $responses = $this->get_actual_response($question, $state);
1114 if (empty($responses) || !is_array($responses)) {
1115 $responses = array();
1117 if (is_array($responses)) {
1118 $responses = implode(',', array_map('s', $responses));
1120 return shorten_text($responses, $length);
1124 * Renders the question for printing and returns the LaTeX source produced
1126 * This function should render the question suitable for a printed problem
1127 * or solution sheet in LaTeX and return the rendered output.
1128 * @return string The LaTeX output.
1129 * @param object $question The question to be rendered. Question type
1130 * specific information is included.
1131 * @param object $state The state to render the question in. The
1132 * question type specific information is also
1133 * included.
1134 * @param object $cmoptions
1135 * @param string $type Indicates if the question or the solution is to be
1136 * rendered with the values 'question' and
1137 * 'solution'.
1139 function get_texsource(&$question, &$state, $cmoptions, $type) {
1140 // The default implementation simply returns a string stating that
1141 // the question is only available online.
1143 return get_string('onlineonly', 'texsheet');
1147 * Compares two question states for equivalence of the student's responses
1149 * The responses for the two states must be examined to see if they represent
1150 * equivalent answers to the question by the student. This method will be
1151 * invoked for each of the previous states of the question before grading
1152 * occurs. If the student is found to have already attempted the question
1153 * with equivalent responses then the attempt at the question is ignored;
1154 * grading does not occur and the state does not change. Thus they are not
1155 * penalized for this case.
1156 * @return boolean
1157 * @param object $question The question for which the states are to be
1158 * compared. Question type specific information is
1159 * included.
1160 * @param object $state The state of the question. The responses are in
1161 * ->responses. This is the only field of $state
1162 * that it is safe to use.
1163 * @param object $teststate The state whose responses are to be
1164 * compared. The state will be of the same age or
1165 * older than $state. If possible, the method should
1166 * only use the field $teststate->responses, however
1167 * any field that is set up by restore_session_and_responses
1168 * can be used.
1170 function compare_responses(&$question, $state, $teststate) {
1171 // The default implementation performs a comparison of the response
1172 // arrays. The ordering of the arrays does not matter.
1173 // Question types may wish to override this (eg. to ignore trailing
1174 // white space or to make "7.0" and "7" compare equal).
1176 // In php neither == nor === compare arrays the way you want. The following
1177 // ensures that the arrays have the same keys, with the same values.
1178 $result = false;
1179 $diff1 = array_diff_assoc($state->responses, $teststate->responses);
1180 if (empty($diff1)) {
1181 $diff2 = array_diff_assoc($teststate->responses, $state->responses);
1182 $result = empty($diff2);
1185 return $result;
1189 * Checks whether a response matches a given answer
1191 * This method only applies to questions that use teacher-defined answers
1193 * @return boolean
1195 function test_response(&$question, &$state, $answer) {
1196 $response = isset($state->responses['']) ? $state->responses[''] : '';
1197 return ($response == $answer->answer);
1201 * Performs response processing and grading
1203 * This function performs response processing and grading and updates
1204 * the state accordingly.
1205 * @return boolean Indicates success or failure.
1206 * @param object $question The question to be graded. Question type
1207 * specific information is included.
1208 * @param object $state The state of the question to grade. The current
1209 * responses are in ->responses. The last graded state
1210 * is in ->last_graded (hence the most recently graded
1211 * responses are in ->last_graded->responses). The
1212 * question type specific information is also
1213 * included. The ->raw_grade and ->penalty fields
1214 * must be updated. The method is able to
1215 * close the question session (preventing any further
1216 * attempts at this question) by setting
1217 * $state->event to QUESTION_EVENTCLOSEANDGRADE
1218 * @param object $cmoptions
1220 function grade_responses(&$question, &$state, $cmoptions) {
1221 // The default implementation uses the test_response method to
1222 // compare what the student entered against each of the possible
1223 // answers stored in the question, and uses the grade from the
1224 // first one that matches. It also sets the marks and penalty.
1225 // This should be good enought for most simple question types.
1227 $state->raw_grade = 0;
1228 foreach($question->options->answers as $answer) {
1229 if($this->test_response($question, $state, $answer)) {
1230 $state->raw_grade = $answer->fraction;
1231 break;
1235 // Make sure we don't assign negative or too high marks.
1236 $state->raw_grade = min(max((float) $state->raw_grade,
1237 0.0), 1.0) * $question->maxgrade;
1239 // Update the penalty.
1240 $state->penalty = $question->penalty * $question->maxgrade;
1242 // mark the state as graded
1243 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
1245 return true;
1250 * Includes configuration settings for the question type on the quiz admin
1251 * page
1253 * TODO: It makes no sense any longer to do the admin for question types
1254 * from the quiz admin page. This should be changed.
1255 * Returns an array of objects describing the options for the question type
1256 * to be included on the quiz module admin page.
1257 * Configuration options can be included by setting the following fields in
1258 * the object:
1259 * ->name The name of the option within this question type.
1260 * The full option name will be constructed as
1261 * "quiz_{$this->name()}_$name", the human readable name
1262 * will be displayed with get_string($name, 'quiz').
1263 * ->code The code to display the form element, help button, etc.
1264 * i.e. the content for the central table cell. Be sure
1265 * to name the element "quiz_{$this->name()}_$name" and
1266 * set the value to $CFG->{"quiz_{$this->name()}_$name"}.
1267 * ->help Name of the string from the quiz module language file
1268 * to be used for the help message in the third column of
1269 * the table. An empty string (or the field not set)
1270 * means to leave the box empty.
1271 * Links to custom settings pages can be included by setting the following
1272 * fields in the object:
1273 * ->name The name of the link text string.
1274 * get_string($name, 'quiz') will be called.
1275 * ->link The filename part of the URL for the link. The full URL
1276 * is contructed as
1277 * "$CFG->wwwroot/question/type/{$this->name()}/$link?sesskey=$sesskey"
1278 * [but with the relavant calls to the s and rawurlencode
1279 * functions] where $sesskey is the sesskey for the user.
1280 * @return array Array of objects describing the configuration options to
1281 * be included on the quiz module admin page.
1283 function get_config_options() {
1284 // No options by default
1286 return false;
1290 * Returns true if the editing wizard is finished, false otherwise.
1292 * The default implementation returns true, which is suitable for all question-
1293 * types that only use one editing form. This function is used in
1294 * question.php to decide whether we can regrade any states of the edited
1295 * question and redirect to edit.php.
1297 * The dataset dependent question-type, which is extended by the calculated
1298 * question-type, overwrites this method because it uses multiple pages (i.e.
1299 * a wizard) to set up the question and associated datasets.
1301 * @param object $form The data submitted by the previous page.
1303 * @return boolean Whether the wizard's last page was submitted or not.
1305 function finished_edit_wizard(&$form) {
1306 //In the default case there is only one edit page.
1307 return true;
1311 * Prints a table of course modules in which the question is used
1313 * TODO: This should be made quiz-independent
1315 * This function is used near the end of the question edit forms in all question types
1316 * It prints the table of quizzes in which the question is used
1317 * containing checkboxes to allow the teacher to replace the old question version
1319 * @param object $question
1320 * @param object $course
1321 * @param integer $cmid optional The id of the course module currently being edited
1323 function print_replacement_options($question, $course, $cmid='0') {
1325 // Disable until the versioning code has been fixed
1326 if (true) {
1327 return;
1330 // no need to display replacement options if the question is new
1331 if(empty($question->id)) {
1332 return true;
1335 // get quizzes using the question (using the question_instances table)
1336 $quizlist = array();
1337 if(!$instances = get_records('quiz_question_instances', 'question', $question->id)) {
1338 $instances = array();
1340 foreach($instances as $instance) {
1341 $quizlist[$instance->quiz] = $instance->quiz;
1343 $quizlist = implode(',', $quizlist);
1344 if(empty($quizlist) or !$quizzes = get_records_list('quiz', 'id', $quizlist)) {
1345 $quizzes = array();
1348 // do the printing
1349 if(count($quizzes) > 0) {
1350 // print the table
1351 $strquizname = get_string('modulename', 'quiz');
1352 $strdoreplace = get_string('replace', 'quiz');
1353 $straffectedstudents = get_string('affectedstudents', 'quiz', $course->students);
1354 echo "<tr valign=\"top\">\n";
1355 echo "<td align=\"right\"><b>".get_string("replacementoptions", "quiz").":</b></td>\n";
1356 echo "<td align=\"left\">\n";
1357 echo "<table cellpadding=\"5\" align=\"left\" class=\"generalbox\" width=\"100%\">\n";
1358 echo "<tr>\n";
1359 echo "<th align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$strquizname</th>\n";
1360 echo "<th align=\"center\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$strdoreplace</th>\n";
1361 echo "<th align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$straffectedstudents</th>\n";
1362 echo "</tr>\n";
1363 foreach($quizzes as $quiz) {
1364 // work out whethere it should be checked by default
1365 $checked = '';
1366 if((int)$cmid === (int)$quiz->id
1367 or empty($quiz->usercount)) {
1368 $checked = "checked=\"checked\"";
1371 // find how many different students have already attempted this quiz
1372 $students = array();
1373 if($attempts = get_records_select('quiz_attempts', "quiz = '$quiz->id' AND preview = '0'")) {
1374 foreach($attempts as $attempt) {
1375 if (record_exists('question_states', 'attempt', $attempt->uniqueid, 'question', $question->id, 'originalquestion', 0)) {
1376 $students[$attempt->userid] = 1;
1380 $studentcount = count($students);
1382 $strstudents = $studentcount === 1 ? $course->student : $course->students;
1383 echo "<tr>\n";
1384 echo "<td align=\"left\" class=\"generaltablecell c0\">".format_string($quiz->name)."</td>\n";
1385 echo "<td align=\"center\" class=\"generaltablecell c0\"><input name=\"q{$quiz->id}replace\" type=\"checkbox\" ".$checked." /></td>\n";
1386 echo "<td align=\"left\" class=\"generaltablecell c0\">".(($studentcount) ? $studentcount.' '.$strstudents : '-')."</td>\n";
1387 echo "</tr>\n";
1389 echo "</table>\n";
1391 echo "</td></tr>\n";
1395 * Call format_text from weblib.php with the options appropriate to question types.
1397 * @param string $text the text to format.
1398 * @param integer $text the type of text. Normally $question->questiontextformat.
1399 * @param object $cmoptions the context the string is being displayed in. Only $cmoptions->course is used.
1400 * @return string the formatted text.
1402 function format_text($text, $textformat, $cmoptions = NULL) {
1403 $formatoptions = new stdClass;
1404 $formatoptions->noclean = true;
1405 $formatoptions->para = false;
1406 return format_text($text, $textformat, $formatoptions, $cmoptions === NULL ? NULL : $cmoptions->course);
1410 * Find all course / site files linked from a question.
1412 * Need to check for links to files in question_answers.answer and feedback
1413 * and in question table in generalfeedback and questiontext fields. Methods
1414 * on child classes will also check extra question specific fields.
1416 * Needs to be overriden for child classes that have extra fields containing
1417 * html.
1419 * @param string html the html to search
1420 * @param int courseid search for files for courseid course or set to siteid for
1421 * finding site files.
1422 * @return array of url, relative url is key and array with one item = question id as value
1423 * relative url is relative to course/site files directory root.
1425 function find_file_links($question, $courseid){
1426 $urls = array();
1428 /// Question image
1429 if ($question->image != ''){
1430 if (substr(strtolower($question->image), 0, 7) == 'http://') {
1431 $matches = array();
1433 //support for older questions where we have a complete url in image field
1434 if (preg_match('!^'.question_file_links_base_url($courseid).'(.*)!i', $question->image, $matches)){
1435 if ($cleanedurl = question_url_check($urls[$matches[2]])){
1436 $urls[$cleanedurl] = null;
1439 } else {
1440 if ($question->image != ''){
1441 if ($cleanedurl = question_url_check($question->image)){
1442 $urls[$cleanedurl] = null;//will be set later
1450 /// Questiontext and general feedback.
1451 $urls += question_find_file_links_from_html($question->questiontext, $courseid);
1452 $urls += question_find_file_links_from_html($question->generalfeedback, $courseid);
1454 /// Answers, if this question uses them.
1455 if (isset($question->options->answers)){
1456 foreach ($question->options->answers as $answerkey => $answer){
1457 /// URLs in the answers themselves, if appropriate.
1458 if ($this->has_html_answers()) {
1459 $urls += question_find_file_links_from_html($answer->answer, $courseid);
1461 /// URLs in the answer feedback.
1462 $urls += question_find_file_links_from_html($answer->feedback, $courseid);
1466 /// Set all the values of the array to the question object
1467 if ($urls){
1468 $urls = array_combine(array_keys($urls), array_fill(0, count($urls), array($question->id)));
1470 return $urls;
1473 * Find all course / site files linked from a question.
1475 * Need to check for links to files in question_answers.answer and feedback
1476 * and in question table in generalfeedback and questiontext fields. Methods
1477 * on child classes will also check extra question specific fields.
1479 * Needs to be overriden for child classes that have extra fields containing
1480 * html.
1482 * @param string html the html to search
1483 * @param int course search for files for courseid course or set to siteid for
1484 * finding site files.
1485 * @return array of files, file name is key and array with one item = question id as value
1487 function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){
1488 global $CFG;
1489 $updateqrec = false;
1491 /// Question image
1492 if (!empty($question->image)){
1493 //support for older questions where we have a complete url in image field
1494 if (substr(strtolower($question->image), 0, 7) == 'http://') {
1495 $questionimage = preg_replace('!^'.question_file_links_base_url($fromcourseid).preg_quote($url, '!').'$!i', $destination, $question->image, 1);
1496 } else {
1497 $questionimage = preg_replace('!^'.preg_quote($url, '!').'$!i', $destination, $question->image, 1);
1499 if ($questionimage != $question->image){
1500 $question->image = $questionimage;
1501 $updateqrec = true;
1505 /// Questiontext and general feedback.
1506 $question->questiontext = question_replace_file_links_in_html($question->questiontext, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
1507 $question->generalfeedback = question_replace_file_links_in_html($question->generalfeedback, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
1509 /// If anything has changed, update it in the database.
1510 if ($updateqrec){
1511 if (!update_record('question', addslashes_recursive($question))){
1512 error ('Couldn\'t update question '.$question->name);
1517 /// Answers, if this question uses them.
1518 if (isset($question->options->answers)){
1519 //answers that do not need updating have been unset
1520 foreach ($question->options->answers as $answer){
1521 $answerchanged = false;
1522 /// URLs in the answers themselves, if appropriate.
1523 if ($this->has_html_answers()) {
1524 $answer->answer = question_replace_file_links_in_html($answer->answer, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
1526 /// URLs in the answer feedback.
1527 $answer->feedback = question_replace_file_links_in_html($answer->feedback, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
1528 /// If anything has changed, update it in the database.
1529 if ($answerchanged){
1530 if (!update_record('question_answers', addslashes_recursive($answer))){
1531 error ('Couldn\'t update question ('.$question->name.') answer '.$answer->id);
1538 * @return the best link to pass to print_error.
1539 * @param $cmoptions as passed in from outside.
1541 function error_link($cmoptions) {
1542 global $CFG;
1543 $cm = get_coursemodule_from_instance('quiz', $cmoptions->id);
1544 if (!empty($cm->id)) {
1545 return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
1546 } else if (!empty($cm->course)) {
1547 return $CFG->wwwroot . '/course/view.php?id=' . $cm->course;
1548 } else {
1549 return '';
1553 /// BACKUP FUNCTIONS ////////////////////////////
1556 * Backup the data in the question
1558 * This is used in question/backuplib.php
1560 function backup($bf,$preferences,$question,$level=6) {
1561 // The default type has nothing to back up
1562 return true;
1565 /// RESTORE FUNCTIONS /////////////////
1568 * Restores the data in the question
1570 * This is used in question/restorelib.php
1572 function restore($old_question_id,$new_question_id,$info,$restore) {
1573 // The default question type has nothing to restore
1574 return true;
1577 function restore_map($old_question_id,$new_question_id,$info,$restore) {
1578 // There is nothing to decode
1579 return true;
1582 function restore_recode_answer($state, $restore) {
1583 // There is nothing to decode
1584 return $state->answer;
1588 * Abstract function implemented by each question type. It runs all the code
1589 * required to set up and save a question of any type for testing purposes.
1590 * Alternate DB table prefix may be used to facilitate data deletion.
1592 function generate_test($name, $courseid=null) {
1593 $form = new stdClass();
1594 $form->name = $name;
1595 $form->questiontextformat = 1;
1596 $form->questiontext = 'test question, generated by script';
1597 $form->defaultgrade = 1;
1598 $form->penalty = 0.1;
1599 $form->generalfeedback = "Well done";
1601 $context = get_context_instance(CONTEXT_COURSE, $courseid);
1602 $newcategory = question_make_default_categories(array($context));
1603 $form->category = $newcategory->id . ',1';
1605 $question = new stdClass();
1606 $question->courseid = $courseid;
1607 $question->qtype = $this->qtype;
1608 return array($form, $question);