2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Code for exporting questions as Moodle XML.
22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->libdir . '/xmlize.php');
30 if (!class_exists('qformat_default')) {
31 // This is ugly, but this class is also (ab)used by mod/lesson, which defines
32 // a different base class in mod/lesson/format.php. Thefore, we can only
33 // include the proper base class conditionally like this. (We have to include
34 // the base class like this, otherwise it breaks third-party question types.)
35 // This may be reviewd, and a better fix found one day.
36 require_once($CFG->dirroot . '/question/format.php');
41 * Importer for Moodle XML question format.
43 * See http://docs.moodle.org/en/Moodle_XML_format for a description of the format.
45 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
48 class qformat_xml extends qformat_default {
50 public function provide_import() {
54 public function provide_export() {
58 public function mime_type() {
59 return 'application/xml';
62 // IMPORT FUNCTIONS START HERE
65 * Translate human readable format name
66 * into internal Moodle code number
67 * @param string name format name from xml file
68 * @return int Moodle format code
70 public function trans_format($name) {
73 if ($name == 'moodle_auto_format') {
75 } else if ($name == 'html') {
77 } else if ($name == 'plain_text') {
79 } else if ($name == 'wiki_like') {
81 } else if ($name == 'markdown') {
82 return FORMAT_MARKDOWN;
84 debugging("Unrecognised text format '{$name}' in the import file. Assuming 'html'.");
90 * Translate human readable single answer option
91 * to internal code number
92 * @param string name true/false
93 * @return int internal code number
95 public function trans_single($name) {
97 if ($name == "false" || !$name) {
105 * process text string from xml file
106 * @param array $text bit of xml tree after ['text']
107 * @return string processed text.
109 public function import_text($text) {
110 // quick sanity check
114 $data = $text[0]['#'];
119 * return the value of a node, given a path to the node
120 * if it doesn't exist return the default value
121 * @param array xml data to read
122 * @param array path path to node expressed as array
123 * @param mixed default
124 * @param bool istext process as text
125 * @param string error if set value must exist, return false and issue message if not
126 * @return mixed value
128 public function getpath($xml, $path, $default, $istext=false, $error='') {
129 foreach ($path as $index) {
130 if (!isset($xml[$index])) {
131 if (!empty($error)) {
132 $this->error($error);
143 if (!is_string($xml)) {
144 $this->error(get_string('invalidxml', 'qformat_xml'));
152 public function import_text_with_files($data, $path, $defaultvalue = '', $defaultformat = 'html') {
154 $field['text'] = $this->getpath($data,
155 array_merge($path, array('#', 'text', 0, '#')), $defaultvalue, true);
156 $field['format'] = $this->trans_format($this->getpath($data,
157 array_merge($path, array('@', 'format')), $defaultformat));
158 $itemid = $this->import_files_as_draft($this->getpath($data,
159 array_merge($path, array('#', 'file')), array(), false));
160 if (!empty($itemid)) {
161 $field['itemid'] = $itemid;
166 public function import_files_as_draft($xml) {
171 $fs = get_file_storage();
172 $itemid = file_get_unused_draft_itemid();
173 $filenames = array();
174 foreach ($xml as $file) {
175 $filename = $file['@']['name'];
176 if (in_array($filename, $filenames)) {
177 debugging('Duplicate file in XML: ' . $filename, DEBUG_DEVELOPER);
181 'contextid' => context_user::instance($USER->id)->id,
182 'component' => 'user',
183 'filearea' => 'draft',
186 'filename' => $filename,
188 $fs->create_file_from_string($filerecord, base64_decode($file['#']));
189 $filenames[] = $filename;
195 * import parts of question common to all types
196 * @param $question array question question array from xml tree
197 * @return object question object
199 public function import_headers($question) {
202 // get some error strings
203 $error_noname = get_string('xmlimportnoname', 'qformat_xml');
204 $error_noquestion = get_string('xmlimportnoquestion', 'qformat_xml');
206 // this routine initialises the question object
207 $qo = $this->defaultquestion();
210 $qo->name = $this->clean_question_name($this->getpath($question,
211 array('#', 'name', 0, '#', 'text', 0, '#'), '', true,
212 get_string('xmlimportnoname', 'qformat_xml')));
213 $questiontext = $this->import_text_with_files($question,
214 array('#', 'questiontext', 0));
215 $qo->questiontext = $questiontext['text'];
216 $qo->questiontextformat = $questiontext['format'];
217 if (!empty($questiontext['itemid'])) {
218 $qo->questiontextitemid = $questiontext['itemid'];
220 // Backwards compatibility, deal with the old image tag.
221 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
222 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
223 if ($filedata && $filename) {
224 $fs = get_file_storage();
225 if (empty($qo->questiontextitemid)) {
226 $qo->questiontextitemid = file_get_unused_draft_itemid();
228 $filename = clean_param(str_replace('/', '_', $filename), PARAM_FILE);
230 'contextid' => context_user::instance($USER->id)->id,
231 'component' => 'user',
232 'filearea' => 'draft',
233 'itemid' => $qo->questiontextitemid,
235 'filename' => $filename,
237 $fs->create_file_from_string($filerecord, base64_decode($filedata));
238 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
241 // restore files in generalfeedback
242 $generalfeedback = $this->import_text_with_files($question,
243 array('#', 'generalfeedback', 0), $qo->generalfeedback, $this->get_format($qo->questiontextformat));
244 $qo->generalfeedback = $generalfeedback['text'];
245 $qo->generalfeedbackformat = $generalfeedback['format'];
246 if (!empty($generalfeedback['itemid'])) {
247 $qo->generalfeedbackitemid = $generalfeedback['itemid'];
250 $qo->defaultmark = $this->getpath($question,
251 array('#', 'defaultgrade', 0, '#'), $qo->defaultmark);
252 $qo->penalty = $this->getpath($question,
253 array('#', 'penalty', 0, '#'), $qo->penalty);
255 // Fix problematic rounding from old files:
256 if (abs($qo->penalty - 0.3333333) < 0.005) {
257 $qo->penalty = 0.3333333;
260 // Read the question tags.
261 if (!empty($CFG->usetags) && array_key_exists('tags', $question['#'])
262 && !empty($question['#']['tags'][0]['#']['tag'])) {
263 require_once($CFG->dirroot.'/tag/lib.php');
265 foreach ($question['#']['tags'][0]['#']['tag'] as $tagdata) {
266 $qo->tags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true);
274 * Import the common parts of a single answer
275 * @param array answer xml tree for single answer
276 * @param bool $withanswerfiles if true, the answers are HTML (or $defaultformat)
277 * and so may contain files, otherwise the answers are plain text.
278 * @param array Default text format for the feedback, and the answers if $withanswerfiles
280 * @return object answer object
282 public function import_answer($answer, $withanswerfiles = false, $defaultformat = 'html') {
283 $ans = new stdClass();
285 if ($withanswerfiles) {
286 $ans->answer = $this->import_text_with_files($answer, array(), '', $defaultformat);
288 $ans->answer = array();
289 $ans->answer['text'] = $this->getpath($answer, array('#', 'text', 0, '#'), '', true);
290 $ans->answer['format'] = FORMAT_PLAIN;
293 $ans->feedback = $this->import_text_with_files($answer, array('#', 'feedback', 0), '', $defaultformat);
295 $ans->fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
301 * Import the common overall feedback fields.
302 * @param object $question the part of the XML relating to this question.
303 * @param object $qo the question data to add the fields to.
304 * @param bool $withshownumpartscorrect include the shownumcorrect field.
306 public function import_combined_feedback($qo, $questionxml, $withshownumpartscorrect = false) {
307 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
308 foreach ($fields as $field) {
309 $qo->$field = $this->import_text_with_files($questionxml,
310 array('#', $field, 0), '', $this->get_format($qo->questiontextformat));
313 if ($withshownumpartscorrect) {
314 $qo->shownumcorrect = array_key_exists('shownumcorrect', $questionxml['#']);
316 // Backwards compatibility:
317 if (array_key_exists('correctresponsesfeedback', $questionxml['#'])) {
318 $qo->shownumcorrect = $this->trans_single($this->getpath($questionxml,
319 array('#', 'correctresponsesfeedback', 0, '#'), 1));
325 * Import a question hint
326 * @param array $hintxml hint xml fragment.
327 * @param string $defaultformat the text format to assume for hints that do not specify.
328 * @return object hint for storing in the database.
330 public function import_hint($hintxml, $defaultformat) {
331 $hint = new stdClass();
332 if (array_key_exists('hintcontent', $hintxml['#'])) {
333 // Backwards compatibility:
335 $hint->hint = $this->import_text_with_files($hintxml,
336 array('#', 'hintcontent', 0), '', $defaultformat);
338 $hint->shownumcorrect = $this->getpath($hintxml,
339 array('#', 'statenumberofcorrectresponses', 0, '#'), 0);
340 $hint->clearwrong = $this->getpath($hintxml,
341 array('#', 'clearincorrectresponses', 0, '#'), 0);
342 $hint->options = $this->getpath($hintxml,
343 array('#', 'showfeedbacktoresponses', 0, '#'), 0);
347 $hint->hint = $this->import_text_with_files($hintxml, array(), '', $defaultformat);
348 $hint->shownumcorrect = array_key_exists('shownumcorrect', $hintxml['#']);
349 $hint->clearwrong = array_key_exists('clearwrong', $hintxml['#']);
350 $hint->options = $this->getpath($hintxml, array('#', 'options', 0, '#'), '', true);
356 * Import all the question hints
358 * @param object $qo the question data that is being constructed.
359 * @param array $questionxml The xml representing the question.
360 * @param bool $withparts whether the extra fields relating to parts should be imported.
361 * @param bool $withoptions whether the extra options field should be imported.
362 * @param string $defaultformat the text format to assume for hints that do not specify.
363 * @return array of objects representing the hints in the file.
365 public function import_hints($qo, $questionxml, $withparts = false,
366 $withoptions = false, $defaultformat = 'html') {
367 if (!isset($questionxml['#']['hint'])) {
371 foreach ($questionxml['#']['hint'] as $hintxml) {
372 $hint = $this->import_hint($hintxml, $defaultformat);
373 $qo->hint[] = $hint->hint;
376 $qo->hintshownumcorrect[] = $hint->shownumcorrect;
377 $qo->hintclearwrong[] = $hint->clearwrong;
381 $qo->hintoptions[] = $hint->options;
387 * Import files from a node in the XML.
388 * @param array $xml an array of <file> nodes from the the parsed XML.
389 * @return array of things representing files - in the form that save_question expects.
391 public function import_files($xml) {
393 foreach ($xml as $file) {
394 $data = new stdClass();
395 $data->content = $file['#'];
396 $data->encoding = $file['@']['encoding'];
397 $data->name = $file['@']['name'];
404 * import multiple choice question
405 * @param array question question array from xml tree
406 * @return object question object
408 public function import_multichoice($question) {
410 $qo = $this->import_headers($question);
412 // 'header' parts particular to multichoice
413 $qo->qtype = MULTICHOICE;
414 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
415 $qo->single = $this->trans_single($single);
416 $shuffleanswers = $this->getpath($question,
417 array('#', 'shuffleanswers', 0, '#'), 'false');
418 $qo->answernumbering = $this->getpath($question,
419 array('#', 'answernumbering', 0, '#'), 'abc');
420 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
422 // There was a time on the 1.8 branch when it could output an empty
423 // answernumbering tag, so fix up any found.
424 if (empty($qo->answernumbering)) {
425 $qo->answernumbering = 'abc';
428 // Run through the answers
429 $answers = $question['#']['answer'];
431 foreach ($answers as $answer) {
432 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
433 $qo->answer[$acount] = $ans->answer;
434 $qo->fraction[$acount] = $ans->fraction;
435 $qo->feedback[$acount] = $ans->feedback;
439 $this->import_combined_feedback($qo, $question, true);
440 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
446 * Import cloze type question
447 * @param array question question array from xml tree
448 * @return object question object
450 public function import_multianswer($question) {
451 question_bank::get_qtype('multianswer');
453 $questiontext['text'] = $this->import_text($question['#']['questiontext'][0]['#']['text']);
454 $questiontext['format'] = FORMAT_HTML;
455 $questiontext['itemid'] = '';
456 $qo = qtype_multianswer_extract_question($questiontext);
458 // 'header' parts particular to multianswer
459 $qo->qtype = 'multianswer';
460 $qo->course = $this->course;
461 $qo->generalfeedback = '';
463 $qo->name = $this->clean_question_name($this->import_text($question['#']['name'][0]['#']['text']));
464 $qo->questiontextformat = $questiontext['format'];
465 $qo->questiontext = $qo->questiontext['text'];
466 $itemid = $this->import_files($this->getpath($question,
467 array('#', 'questiontext', 0, '#', 'file'), array(), false));
468 if (!empty($itemid)) {
469 $qo->questiontextitemid = $itemid;
471 // Backwards compatibility, deal with the old image tag.
472 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
473 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
474 if ($filedata && $filename) {
475 $data = new stdClass();
476 $data->content = $filedata;
477 $data->encoding = 'base64';
478 // Question file areas don't support subdirs, so convert path to filename if necessary.
479 $data->name = clean_param(str_replace('/', '_', $filename), PARAM_FILE);
480 $qo->questiontextfiles[] = $data;
481 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $data->name . '" />';
484 // restore files in generalfeedback
485 $generalfeedback = $this->import_text_with_files($question,
486 array('#', 'generalfeedback', 0), $qo->generalfeedback, $this->get_format($qo->questiontextformat));
487 $qo->generalfeedback = $generalfeedback['text'];
488 $qo->generalfeedbackformat = $generalfeedback['format'];
489 if (!empty($generalfeedback['itemid'])) {
490 $qo->generalfeedbackitemid = $generalfeedback['itemid'];
493 $qo->penalty = $this->getpath($question,
494 array('#', 'penalty', 0, '#'), $this->defaultquestion()->penalty);
495 // Fix problematic rounding from old files:
496 if (abs($qo->penalty - 0.3333333) < 0.005) {
497 $qo->penalty = 0.3333333;
500 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
506 * Import true/false type question
507 * @param array question question array from xml tree
508 * @return object question object
510 public function import_truefalse($question) {
513 $qo = $this->import_headers($question);
515 // 'header' parts particular to true/false
516 $qo->qtype = TRUEFALSE;
518 // In the past, it used to be assumed that the two answers were in the file
519 // true first, then false. Howevever that was not always true. Now, we
520 // try to match on the answer text, but in old exports, this will be a localised
521 // string, so if we don't find true or false, we fall back to the old system.
524 foreach ($question['#']['answer'] as $answer) {
525 $answertext = $this->getpath($answer,
526 array('#', 'text', 0, '#'), '', true);
527 $feedback = $this->import_text_with_files($answer,
528 array('#', 'feedback', 0), '', $this->get_format($qo->questiontextformat));
530 if ($answertext != 'true' && $answertext != 'false') {
531 // Old style file, assume order is true/false.
534 $answertext = 'true';
536 $answertext = 'false';
540 if ($answertext == 'true') {
541 $qo->answer = ($answer['@']['fraction'] == 100);
542 $qo->correctanswer = $qo->answer;
543 $qo->feedbacktrue = $feedback;
545 $qo->answer = ($answer['@']['fraction'] != 100);
546 $qo->correctanswer = $qo->answer;
547 $qo->feedbackfalse = $feedback;
554 $a->questiontext = $qo->questiontext;
555 $a->answer = get_string($qo->correctanswer ? 'true' : 'false', 'qtype_truefalse');
556 echo $OUTPUT->notification(get_string('truefalseimporterror', 'qformat_xml', $a));
559 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
565 * Import short answer type question
566 * @param array question question array from xml tree
567 * @return object question object
569 public function import_shortanswer($question) {
571 $qo = $this->import_headers($question);
573 // header parts particular to shortanswer
574 $qo->qtype = SHORTANSWER;
577 $qo->usecase = $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase);
579 // Run through the answers
580 $answers = $question['#']['answer'];
582 foreach ($answers as $answer) {
583 $ans = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
584 $qo->answer[$acount] = $ans->answer['text'];
585 $qo->fraction[$acount] = $ans->fraction;
586 $qo->feedback[$acount] = $ans->feedback;
590 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
596 * Import description type question
597 * @param array question question array from xml tree
598 * @return object question object
600 public function import_description($question) {
602 $qo = $this->import_headers($question);
603 // header parts particular to shortanswer
604 $qo->qtype = DESCRIPTION;
605 $qo->defaultmark = 0;
611 * Import numerical type question
612 * @param array question question array from xml tree
613 * @return object question object
615 public function import_numerical($question) {
617 $qo = $this->import_headers($question);
619 // header parts particular to numerical
620 $qo->qtype = NUMERICAL;
623 $answers = $question['#']['answer'];
624 $qo->answer = array();
625 $qo->feedback = array();
626 $qo->fraction = array();
627 $qo->tolerance = array();
628 foreach ($answers as $answer) {
629 // answer outside of <text> is deprecated
630 $obj = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
631 $qo->answer[] = $obj->answer['text'];
632 if (empty($qo->answer)) {
635 $qo->feedback[] = $obj->feedback;
636 $qo->tolerance[] = $this->getpath($answer, array('#', 'tolerance', 0, '#'), 0);
638 // fraction as a tag is deprecated
639 $fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
640 $qo->fraction[] = $this->getpath($answer,
641 array('#', 'fraction', 0, '#'), $fraction); // deprecated
644 // Get the units array
646 $units = $this->getpath($question, array('#', 'units', 0, '#', 'unit'), array());
647 if (!empty($units)) {
648 $qo->multiplier = array();
649 foreach ($units as $unit) {
650 $qo->multiplier[] = $this->getpath($unit, array('#', 'multiplier', 0, '#'), 1);
651 $qo->unit[] = $this->getpath($unit, array('#', 'unit_name', 0, '#'), '', true);
654 $qo->unitgradingtype = $this->getpath($question, array('#', 'unitgradingtype', 0, '#'), 0);
655 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0.1);
656 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), null);
657 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
658 $qo->instructions['text'] = '';
659 $qo->instructions['format'] = FORMAT_HTML;
660 $instructions = $this->getpath($question, array('#', 'instructions'), array());
661 if (!empty($instructions)) {
662 $qo->instructions = $this->import_text_with_files($instructions,
663 array('0'), '', $this->get_format($qo->questiontextformat));
666 if (is_null($qo->showunits)) {
667 // Set a good default, depending on whether there are any units defined.
668 if (empty($qo->unit)) {
669 $qo->showunits = 3; // qtype_numerical::UNITNONE;
671 $qo->showunits = 0; // qtype_numerical::UNITOPTIONAL;
675 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
681 * Import matching type question
682 * @param array question question array from xml tree
683 * @return object question object
685 public function import_match($question) {
687 $qo = $this->import_headers($question);
689 // header parts particular to matching
690 $qo->qtype = 'match';
691 $qo->shuffleanswers = $this->trans_single($this->getpath($question,
692 array('#', 'shuffleanswers', 0, '#'), 1));
694 // run through subquestions
695 $qo->subquestions = array();
696 $qo->subanswers = array();
697 foreach ($question['#']['subquestion'] as $subqxml) {
698 $qo->subquestions[] = $this->import_text_with_files($subqxml,
699 array(), '', $this->get_format($qo->questiontextformat));
701 $answers = $this->getpath($subqxml, array('#', 'answer'), array());
702 $qo->subanswers[] = $this->getpath($subqxml,
703 array('#', 'answer', 0, '#', 'text', 0, '#'), '', true);
706 $this->import_combined_feedback($qo, $question, true);
707 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
713 * Import essay type question
714 * @param array question question array from xml tree
715 * @return object question object
717 public function import_essay($question) {
719 $qo = $this->import_headers($question);
721 // header parts particular to essay
724 $qo->responseformat = $this->getpath($question,
725 array('#', 'responseformat', 0, '#'), 'editor');
726 $qo->responsefieldlines = $this->getpath($question,
727 array('#', 'responsefieldlines', 0, '#'), 15);
728 $qo->attachments = $this->getpath($question,
729 array('#', 'attachments', 0, '#'), 0);
730 $qo->graderinfo = $this->import_text_with_files($question,
731 array('#', 'graderinfo', 0), '', $this->get_format($qo->questiontextformat));
737 * Import a calculated question
738 * @param object $question the imported XML data.
740 public function import_calculated($question) {
743 $qo = $this->import_headers($question);
745 // header parts particular to calculated
746 $qo->qtype = CALCULATED;
747 $qo->synchronize = $this->getpath($question, array('#', 'synchronize', 0, '#'), 0);
748 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
749 $qo->single = $this->trans_single($single);
750 $shuffleanswers = $this->getpath($question, array('#', 'shuffleanswers', 0, '#'), 'false');
751 $qo->answernumbering = $this->getpath($question,
752 array('#', 'answernumbering', 0, '#'), 'abc');
753 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
755 $this->import_combined_feedback($qo, $question);
757 $qo->unitgradingtype = $this->getpath($question,
758 array('#', 'unitgradingtype', 0, '#'), 0);
759 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), null);
760 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
761 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
762 $qo->instructions = $this->getpath($question,
763 array('#', 'instructions', 0, '#', 'text', 0, '#'), '', true);
764 if (!empty($instructions)) {
765 $qo->instructions = $this->import_text_with_files($instructions,
766 array('0'), '', $this->get_format($qo->questiontextformat));
770 $answers = $question['#']['answer'];
771 $qo->answers = array();
772 $qo->feedback = array();
773 $qo->fraction = array();
774 $qo->tolerance = array();
775 $qo->tolerancetype = array();
776 $qo->correctanswerformat = array();
777 $qo->correctanswerlength = array();
778 $qo->feedback = array();
779 foreach ($answers as $answer) {
780 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
781 // answer outside of <text> is deprecated
782 if (empty($ans->answer['text'])) {
783 $ans->answer['text'] = '*';
785 $qo->answers[] = $ans->answer;
786 $qo->feedback[] = $ans->feedback;
787 $qo->tolerance[] = $answer['#']['tolerance'][0]['#'];
788 // fraction as a tag is deprecated
789 if (!empty($answer['#']['fraction'][0]['#'])) {
790 $qo->fraction[] = $answer['#']['fraction'][0]['#'];
792 $qo->fraction[] = $answer['@']['fraction'] / 100;
794 $qo->tolerancetype[] = $answer['#']['tolerancetype'][0]['#'];
795 $qo->correctanswerformat[] = $answer['#']['correctanswerformat'][0]['#'];
796 $qo->correctanswerlength[] = $answer['#']['correctanswerlength'][0]['#'];
800 if (isset($question['#']['units'][0]['#']['unit'])) {
801 $units = $question['#']['units'][0]['#']['unit'];
802 $qo->multiplier = array();
803 foreach ($units as $unit) {
804 $qo->multiplier[] = $unit['#']['multiplier'][0]['#'];
805 $qo->unit[] = $unit['#']['unit_name'][0]['#'];
808 $instructions = $this->getpath($question, array('#', 'instructions'), array());
809 if (!empty($instructions)) {
810 $qo->instructions = $this->import_text_with_files($instructions,
811 array('0'), '', $this->get_format($qo->questiontextformat));
814 if (is_null($qo->unitpenalty)) {
815 // Set a good default, depending on whether there are any units defined.
816 if (empty($qo->unit)) {
817 $qo->showunits = 3; // qtype_numerical::UNITNONE;
819 $qo->showunits = 0; // qtype_numerical::UNITOPTIONAL;
823 $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition'];
824 $qo->dataset = array();
825 $qo->datasetindex= 0;
826 foreach ($datasets as $dataset) {
828 $qo->dataset[$qo->datasetindex] = new stdClass();
829 $qo->dataset[$qo->datasetindex]->status =
830 $this->import_text($dataset['#']['status'][0]['#']['text']);
831 $qo->dataset[$qo->datasetindex]->name =
832 $this->import_text($dataset['#']['name'][0]['#']['text']);
833 $qo->dataset[$qo->datasetindex]->type =
834 $dataset['#']['type'][0]['#'];
835 $qo->dataset[$qo->datasetindex]->distribution =
836 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
837 $qo->dataset[$qo->datasetindex]->max =
838 $this->import_text($dataset['#']['maximum'][0]['#']['text']);
839 $qo->dataset[$qo->datasetindex]->min =
840 $this->import_text($dataset['#']['minimum'][0]['#']['text']);
841 $qo->dataset[$qo->datasetindex]->length =
842 $this->import_text($dataset['#']['decimals'][0]['#']['text']);
843 $qo->dataset[$qo->datasetindex]->distribution =
844 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
845 $qo->dataset[$qo->datasetindex]->itemcount = $dataset['#']['itemcount'][0]['#'];
846 $qo->dataset[$qo->datasetindex]->datasetitem = array();
847 $qo->dataset[$qo->datasetindex]->itemindex = 0;
848 $qo->dataset[$qo->datasetindex]->number_of_items =
849 $dataset['#']['number_of_items'][0]['#'];
850 $datasetitems = $dataset['#']['dataset_items'][0]['#']['dataset_item'];
851 foreach ($datasetitems as $datasetitem) {
852 $qo->dataset[$qo->datasetindex]->itemindex++;
853 $qo->dataset[$qo->datasetindex]->datasetitem[
854 $qo->dataset[$qo->datasetindex]->itemindex] = new stdClass();
855 $qo->dataset[$qo->datasetindex]->datasetitem[
856 $qo->dataset[$qo->datasetindex]->itemindex]->itemnumber =
857 $datasetitem['#']['number'][0]['#'];
858 $qo->dataset[$qo->datasetindex]->datasetitem[
859 $qo->dataset[$qo->datasetindex]->itemindex]->value =
860 $datasetitem['#']['value'][0]['#'];
864 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
870 * This is not a real question type. It's a dummy type used to specify the
871 * import category. The format is:
872 * <question type="category">
873 * <category>tom/dick/harry</category>
876 protected function import_category($question) {
877 $qo = new stdClass();
878 $qo->qtype = 'category';
879 $qo->category = $this->import_text($question['#']['category'][0]['#']['text']);
884 * Parse the array of lines into an array of questions
885 * this *could* burn memory - but it won't happen that much
886 * so fingers crossed!
887 * @param array of lines from the input file.
888 * @param stdClass $context
889 * @return array (of objects) question objects.
891 protected function readquestions($lines) {
892 // We just need it as one big string
893 $lines = implode('', $lines);
895 // This converts xml to big nasty data structure
896 // the 0 means keep white space as it is (important for markdown format)
898 $xml = xmlize($lines, 0, 'UTF-8', true);
899 } catch (xml_format_exception $e) {
900 $this->error($e->getMessage(), '');
903 unset($lines); // No need to keep this in memory.
905 // Set up array to hold all our questions
906 $questions = array();
908 // Iterate through questions
909 foreach ($xml['quiz']['#']['question'] as $question) {
910 $questiontype = $question['@']['type'];
912 if ($questiontype == 'multichoice') {
913 $qo = $this->import_multichoice($question);
914 } else if ($questiontype == 'truefalse') {
915 $qo = $this->import_truefalse($question);
916 } else if ($questiontype == 'shortanswer') {
917 $qo = $this->import_shortanswer($question);
918 } else if ($questiontype == 'numerical') {
919 $qo = $this->import_numerical($question);
920 } else if ($questiontype == 'description') {
921 $qo = $this->import_description($question);
922 } else if ($questiontype == 'matching' || $questiontype == 'match') {
923 $qo = $this->import_match($question);
924 } else if ($questiontype == 'cloze' || $questiontype == 'multianswer') {
925 $qo = $this->import_multianswer($question);
926 } else if ($questiontype == 'essay') {
927 $qo = $this->import_essay($question);
928 } else if ($questiontype == 'calculated') {
929 $qo = $this->import_calculated($question);
930 } else if ($questiontype == 'calculatedsimple') {
931 $qo = $this->import_calculated($question);
932 $qo->qtype = 'calculatedsimple';
933 } else if ($questiontype == 'calculatedmulti') {
934 $qo = $this->import_calculated($question);
935 $qo->qtype = 'calculatedmulti';
936 } else if ($questiontype == 'category') {
937 $qo = $this->import_category($question);
940 // Not a type we handle ourselves. See if the question type wants
942 if (!$qo = $this->try_importing_using_qtypes(
943 $question, null, null, $questiontype)) {
944 $this->error(get_string('xmltypeunsupported', 'qformat_xml', $questiontype));
949 // Stick the result in the $questions array
957 // EXPORT FUNCTIONS START HERE
959 public function export_file_extension() {
964 * Turn the internal question type name into a human readable form.
965 * (In the past, the code used to use integers internally. Now, it uses
966 * strings, so there is less need for this, but to maintain
967 * backwards-compatibility we change two of the type names.)
968 * @param string $qtype question type plugin name.
969 * @return string $qtype string to use in the file.
971 protected function get_qtype($qtype) {
983 * Convert internal Moodle text format code into
984 * human readable form
985 * @param int id internal code
986 * @return string format text
988 public function get_format($id) {
991 return 'moodle_auto_format';
998 case FORMAT_MARKDOWN:
1006 * Convert internal single question code into
1007 * human readable form
1008 * @param int id single question code
1009 * @return string single question string
1011 public function get_single($id) {
1023 * Take a string, and wrap it in a CDATA secion, if that is required to make
1024 * the output XML valid.
1025 * @param string $string a string
1026 * @return string the string, wrapped in CDATA if necessary.
1028 public function xml_escape($string) {
1029 if (!empty($string) && htmlspecialchars($string) != $string) {
1030 return "<![CDATA[{$string}]]>";
1037 * Generates <text></text> tags, processing raw text therein
1038 * @param string $raw the content to output.
1039 * @param int $indent the current indent level.
1040 * @param bool $short stick it on one line.
1041 * @return string formatted text.
1043 public function writetext($raw, $indent = 0, $short = true) {
1044 $indent = str_repeat(' ', $indent);
1045 $raw = $this->xml_escape($raw);
1048 $xml = "$indent<text>$raw</text>\n";
1050 $xml = "$indent<text>\n$raw\n$indent</text>\n";
1057 * Generte the XML to represent some files.
1058 * @param array of store array of stored_file objects.
1059 * @return string $string the XML.
1061 public function write_files($files) {
1062 if (empty($files)) {
1066 foreach ($files as $file) {
1067 if ($file->is_directory()) {
1070 $string .= '<file name="' . $file->get_filename() . '" encoding="base64">';
1071 $string .= base64_encode($file->get_content());
1072 $string .= '</file>';
1077 protected function presave_process($content) {
1078 // Override to allow us to add xml headers and footers
1079 return '<?xml version="1.0" encoding="UTF-8"?>
1081 ' . $content . '</quiz>';
1085 * Turns question into an xml segment
1086 * @param object $question the question data.
1087 * @return string xml segment
1089 public function writequestion($question) {
1090 global $CFG, $OUTPUT;
1092 $fs = get_file_storage();
1093 $contextid = $question->contextid;
1094 // Get files used by the questiontext.
1095 $question->questiontextfiles = $fs->get_area_files(
1096 $contextid, 'question', 'questiontext', $question->id);
1097 // Get files used by the generalfeedback.
1098 $question->generalfeedbackfiles = $fs->get_area_files(
1099 $contextid, 'question', 'generalfeedback', $question->id);
1100 if (!empty($question->options->answers)) {
1101 foreach ($question->options->answers as $answer) {
1102 $answer->answerfiles = $fs->get_area_files(
1103 $contextid, 'question', 'answer', $answer->id);
1104 $answer->feedbackfiles = $fs->get_area_files(
1105 $contextid, 'question', 'answerfeedback', $answer->id);
1111 // Add a comment linking this to the original question id.
1112 $expout .= "<!-- question: $question->id -->\n";
1114 // Check question type
1115 $questiontype = $this->get_qtype($question->qtype);
1117 // Categories are a special case.
1118 if ($question->qtype == 'category') {
1119 $categorypath = $this->writetext($question->category);
1120 $expout .= " <question type=\"category\">\n";
1121 $expout .= " <category>\n";
1122 $expout .= " $categorypath\n";
1123 $expout .= " </category>\n";
1124 $expout .= " </question>\n";
1128 // Now we know we are are handing a real question.
1129 // Output the generic information.
1130 $expout .= " <question type=\"$questiontype\">\n";
1131 $expout .= " <name>\n";
1132 $expout .= $this->writetext($question->name, 3);
1133 $expout .= " </name>\n";
1134 $expout .= " <questiontext {$this->format($question->questiontextformat)}>\n";
1135 $expout .= $this->writetext($question->questiontext, 3);
1136 $expout .= $this->write_files($question->questiontextfiles);
1137 $expout .= " </questiontext>\n";
1138 $expout .= " <generalfeedback {$this->format($question->generalfeedbackformat)}>\n";
1139 $expout .= $this->writetext($question->generalfeedback, 3);
1140 $expout .= $this->write_files($question->generalfeedbackfiles);
1141 $expout .= " </generalfeedback>\n";
1142 if ($question->qtype != 'multianswer') {
1143 $expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
1145 $expout .= " <penalty>{$question->penalty}</penalty>\n";
1146 $expout .= " <hidden>{$question->hidden}</hidden>\n";
1148 // The rest of the output depends on question type.
1149 switch($question->qtype) {
1151 // not a qtype really - dummy used for category switching
1155 $trueanswer = $question->options->answers[$question->options->trueanswer];
1156 $trueanswer->answer = 'true';
1157 $expout .= $this->write_answer($trueanswer);
1159 $falseanswer = $question->options->answers[$question->options->falseanswer];
1160 $falseanswer->answer = 'false';
1161 $expout .= $this->write_answer($falseanswer);
1165 $expout .= " <single>" . $this->get_single($question->options->single) .
1167 $expout .= " <shuffleanswers>" .
1168 $this->get_single($question->options->shuffleanswers) .
1169 "</shuffleanswers>\n";
1170 $expout .= " <answernumbering>" . $question->options->answernumbering .
1171 "</answernumbering>\n";
1172 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
1173 $expout .= $this->write_answers($question->options->answers);
1177 $expout .= " <usecase>{$question->options->usecase}</usecase>\n";
1178 $expout .= $this->write_answers($question->options->answers);
1182 foreach ($question->options->answers as $answer) {
1183 $expout .= $this->write_answer($answer,
1184 " <tolerance>$answer->tolerance</tolerance>\n");
1187 $units = $question->options->units;
1188 if (count($units)) {
1189 $expout .= "<units>\n";
1190 foreach ($units as $unit) {
1191 $expout .= " <unit>\n";
1192 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1193 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1194 $expout .= " </unit>\n";
1196 $expout .= "</units>\n";
1198 if (isset($question->options->unitgradingtype)) {
1199 $expout .= " <unitgradingtype>" . $question->options->unitgradingtype .
1200 "</unitgradingtype>\n";
1202 if (isset($question->options->unitpenalty)) {
1203 $expout .= " <unitpenalty>{$question->options->unitpenalty}</unitpenalty>\n";
1205 if (isset($question->options->showunits)) {
1206 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1208 if (isset($question->options->unitsleft)) {
1209 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1211 if (!empty($question->options->instructionsformat)) {
1212 $files = $fs->get_area_files($contextid, 'qtype_numerical',
1213 'instruction', $question->id);
1214 $expout .= " <instructions " .
1215 $this->format($question->options->instructionsformat) . ">\n";
1216 $expout .= $this->writetext($question->options->instructions, 3);
1217 $expout .= $this->write_files($files);
1218 $expout .= " </instructions>\n";
1223 $expout .= " <shuffleanswers>" .
1224 $this->get_single($question->options->shuffleanswers) .
1225 "</shuffleanswers>\n";
1226 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
1227 foreach ($question->options->subquestions as $subquestion) {
1228 $files = $fs->get_area_files($contextid, 'qtype_match',
1229 'subquestion', $subquestion->id);
1230 $expout .= " <subquestion " .
1231 $this->format($subquestion->questiontextformat) . ">\n";
1232 $expout .= $this->writetext($subquestion->questiontext, 3);
1233 $expout .= $this->write_files($files);
1234 $expout .= " <answer>\n";
1235 $expout .= $this->writetext($subquestion->answertext, 4);
1236 $expout .= " </answer>\n";
1237 $expout .= " </subquestion>\n";
1242 // Nothing else to do.
1246 foreach ($question->options->questions as $index => $subq) {
1247 $expout = preg_replace('~{#' . $index . '}~', $subq->questiontext, $expout);
1252 $expout .= " <responseformat>" . $question->options->responseformat .
1253 "</responseformat>\n";
1254 $expout .= " <responsefieldlines>" . $question->options->responsefieldlines .
1255 "</responsefieldlines>\n";
1256 $expout .= " <attachments>" . $question->options->attachments .
1258 $expout .= " <graderinfo " .
1259 $this->format($question->options->graderinfoformat) . ">\n";
1260 $expout .= $this->writetext($question->options->graderinfo, 3);
1261 $expout .= $this->write_files($fs->get_area_files($contextid, 'qtype_essay',
1262 'graderinfo', $question->id));
1263 $expout .= " </graderinfo>\n";
1267 case 'calculatedsimple':
1268 case 'calculatedmulti':
1269 $expout .= " <synchronize>{$question->options->synchronize}</synchronize>\n";
1270 $expout .= " <single>{$question->options->single}</single>\n";
1271 $expout .= " <answernumbering>" . $question->options->answernumbering .
1272 "</answernumbering>\n";
1273 $expout .= " <shuffleanswers>" . $question->options->shuffleanswers .
1274 "</shuffleanswers>\n";
1276 $component = 'qtype_' . $question->qtype;
1277 $files = $fs->get_area_files($contextid, $component,
1278 'correctfeedback', $question->id);
1279 $expout .= " <correctfeedback>\n";
1280 $expout .= $this->writetext($question->options->correctfeedback, 3);
1281 $expout .= $this->write_files($files);
1282 $expout .= " </correctfeedback>\n";
1284 $files = $fs->get_area_files($contextid, $component,
1285 'partiallycorrectfeedback', $question->id);
1286 $expout .= " <partiallycorrectfeedback>\n";
1287 $expout .= $this->writetext($question->options->partiallycorrectfeedback, 3);
1288 $expout .= $this->write_files($files);
1289 $expout .= " </partiallycorrectfeedback>\n";
1291 $files = $fs->get_area_files($contextid, $component,
1292 'incorrectfeedback', $question->id);
1293 $expout .= " <incorrectfeedback>\n";
1294 $expout .= $this->writetext($question->options->incorrectfeedback, 3);
1295 $expout .= $this->write_files($files);
1296 $expout .= " </incorrectfeedback>\n";
1298 foreach ($question->options->answers as $answer) {
1299 $percent = 100 * $answer->fraction;
1300 $expout .= "<answer fraction=\"$percent\">\n";
1301 // "<text/>" tags are an added feature, old files won't have them
1302 $expout .= " <text>{$answer->answer}</text>\n";
1303 $expout .= " <tolerance>{$answer->tolerance}</tolerance>\n";
1304 $expout .= " <tolerancetype>{$answer->tolerancetype}</tolerancetype>\n";
1305 $expout .= " <correctanswerformat>" .
1306 $answer->correctanswerformat . "</correctanswerformat>\n";
1307 $expout .= " <correctanswerlength>" .
1308 $answer->correctanswerlength . "</correctanswerlength>\n";
1309 $expout .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1310 $files = $fs->get_area_files($contextid, $component,
1311 'instruction', $question->id);
1312 $expout .= $this->writetext($answer->feedback);
1313 $expout .= $this->write_files($answer->feedbackfiles);
1314 $expout .= " </feedback>\n";
1315 $expout .= "</answer>\n";
1317 if (isset($question->options->unitgradingtype)) {
1318 $expout .= " <unitgradingtype>" .
1319 $question->options->unitgradingtype . "</unitgradingtype>\n";
1321 if (isset($question->options->unitpenalty)) {
1322 $expout .= " <unitpenalty>" .
1323 $question->options->unitpenalty . "</unitpenalty>\n";
1325 if (isset($question->options->showunits)) {
1326 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1328 if (isset($question->options->unitsleft)) {
1329 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1332 if (isset($question->options->instructionsformat)) {
1333 $files = $fs->get_area_files($contextid, $component,
1334 'instruction', $question->id);
1335 $expout .= " <instructions " .
1336 $this->format($question->options->instructionsformat) . ">\n";
1337 $expout .= $this->writetext($question->options->instructions, 3);
1338 $expout .= $this->write_files($files);
1339 $expout .= " </instructions>\n";
1342 if (isset($question->options->units)) {
1343 $units = $question->options->units;
1344 if (count($units)) {
1345 $expout .= "<units>\n";
1346 foreach ($units as $unit) {
1347 $expout .= " <unit>\n";
1348 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1349 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1350 $expout .= " </unit>\n";
1352 $expout .= "</units>\n";
1356 // The tag $question->export_process has been set so we get all the
1357 // data items in the database from the function
1358 // qtype_calculated::get_question_options calculatedsimple defaults
1360 if (isset($question->options->datasets) && count($question->options->datasets)) {
1361 $expout .= "<dataset_definitions>\n";
1362 foreach ($question->options->datasets as $def) {
1363 $expout .= "<dataset_definition>\n";
1364 $expout .= " <status>".$this->writetext($def->status)."</status>\n";
1365 $expout .= " <name>".$this->writetext($def->name)."</name>\n";
1366 if ($question->qtype == CALCULATED) {
1367 $expout .= " <type>calculated</type>\n";
1369 $expout .= " <type>calculatedsimple</type>\n";
1371 $expout .= " <distribution>" . $this->writetext($def->distribution) .
1372 "</distribution>\n";
1373 $expout .= " <minimum>" . $this->writetext($def->minimum) .
1375 $expout .= " <maximum>" . $this->writetext($def->maximum) .
1377 $expout .= " <decimals>" . $this->writetext($def->decimals) .
1379 $expout .= " <itemcount>$def->itemcount</itemcount>\n";
1380 if ($def->itemcount > 0) {
1381 $expout .= " <dataset_items>\n";
1382 foreach ($def->items as $item) {
1383 $expout .= " <dataset_item>\n";
1384 $expout .= " <number>".$item->itemnumber."</number>\n";
1385 $expout .= " <value>".$item->value."</value>\n";
1386 $expout .= " </dataset_item>\n";
1388 $expout .= " </dataset_items>\n";
1389 $expout .= " <number_of_items>" . $def->number_of_items .
1390 "</number_of_items>\n";
1392 $expout .= "</dataset_definition>\n";
1394 $expout .= "</dataset_definitions>\n";
1399 // try support by optional plugin
1400 if (!$data = $this->try_exporting_using_qtypes($question->qtype, $question)) {
1401 notify(get_string('unsupportedexport', 'qformat_xml', $question->qtype));
1406 // Output any hints.
1407 $expout .= $this->write_hints($question);
1409 // Write the question tags.
1410 if (!empty($CFG->usetags)) {
1411 require_once($CFG->dirroot.'/tag/lib.php');
1412 $tags = tag_get_tags_array('question', $question->id);
1413 if (!empty($tags)) {
1414 $expout .= " <tags>\n";
1415 foreach ($tags as $tag) {
1416 $expout .= " <tag>" . $this->writetext($tag, 0, true) . "</tag>\n";
1418 $expout .= " </tags>\n";
1422 // close the question tag
1423 $expout .= " </question>\n";
1428 public function write_answers($answers) {
1429 if (empty($answers)) {
1433 foreach ($answers as $answer) {
1434 $output .= $this->write_answer($answer);
1439 public function write_answer($answer, $extra = '') {
1440 $percent = $answer->fraction * 100;
1442 $output .= " <answer fraction=\"$percent\" {$this->format($answer->answerformat)}>\n";
1443 $output .= $this->writetext($answer->answer, 3);
1444 $output .= $this->write_files($answer->answerfiles);
1445 $output .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1446 $output .= $this->writetext($answer->feedback, 4);
1447 $output .= $this->write_files($answer->feedbackfiles);
1448 $output .= " </feedback>\n";
1450 $output .= " </answer>\n";
1455 * Write out the hints.
1456 * @param object $question the question definition data.
1457 * @return string XML to output.
1459 public function write_hints($question) {
1460 if (empty($question->hints)) {
1465 foreach ($question->hints as $hint) {
1466 $output .= $this->write_hint($hint, $question->contextid);
1472 * @param int $format a FORMAT_... constant.
1473 * @return string the attribute to add to an XML tag.
1475 public function format($format) {
1476 return 'format="' . $this->get_format($format) . '"';
1479 public function write_hint($hint, $contextid) {
1480 $fs = get_file_storage();
1481 $files = $fs->get_area_files($contextid, 'question', 'hint', $hint->id);
1484 $output .= " <hint {$this->format($hint->hintformat)}>\n";
1485 $output .= ' ' . $this->writetext($hint->hint);
1487 if (!empty($hint->shownumcorrect)) {
1488 $output .= " <shownumcorrect/>\n";
1490 if (!empty($hint->clearwrong)) {
1491 $output .= " <clearwrong/>\n";
1494 if (!empty($hint->options)) {
1495 $output .= ' <options>' . $this->xml_escape($hint->options) . "</options>\n";
1497 $output .= $this->write_files($files);
1498 $output .= " </hint>\n";
1503 * Output the combined feedback fields.
1504 * @param object $questionoptions the question definition data.
1505 * @param int $questionid the question id.
1506 * @param int $contextid the question context id.
1507 * @return string XML to output.
1509 public function write_combined_feedback($questionoptions, $questionid, $contextid) {
1510 $fs = get_file_storage();
1513 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
1514 foreach ($fields as $field) {
1515 $formatfield = $field . 'format';
1516 $files = $fs->get_area_files($contextid, 'question', $field, $questionid);
1518 $output .= " <{$field} {$this->format($questionoptions->$formatfield)}>\n";
1519 $output .= ' ' . $this->writetext($questionoptions->$field);
1520 $output .= $this->write_files($files);
1521 $output .= " </{$field}>\n";
1524 if (!empty($questionoptions->shownumcorrect)) {
1525 $output .= " <shownumcorrect/>\n";