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.
20 * @package qformat_xml
21 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->libdir . '/xmlize.php');
29 if (!class_exists('qformat_default')) {
30 // This is ugly, but this class is also (ab)used by mod/lesson, which defines
31 // a different base class in mod/lesson/format.php. Thefore, we can only
32 // include the proper base class conditionally like this. (We have to include
33 // the base class like this, otherwise it breaks third-party question types.)
34 // This may be reviewd, and a better fix found one day.
35 require_once($CFG->dirroot . '/question/format.php');
40 * Importer for Moodle XML question format.
42 * See http://docs.moodle.org/en/Moodle_XML_format for a description of the format.
44 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
45 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
47 class qformat_xml extends qformat_default {
49 public function provide_import() {
53 public function provide_export() {
57 public function mime_type() {
58 return 'application/xml';
61 // IMPORT FUNCTIONS START HERE.
64 * Translate human readable format name
65 * into internal Moodle code number
66 * @param string name format name from xml file
67 * @return int Moodle format code
69 public function trans_format($name) {
72 if ($name == 'moodle_auto_format') {
74 } else if ($name == 'html') {
76 } else if ($name == 'plain_text') {
78 } else if ($name == 'wiki_like') {
80 } else if ($name == 'markdown') {
81 return FORMAT_MARKDOWN;
83 debugging("Unrecognised text format '{$name}' in the import file. Assuming 'html'.");
89 * Translate human readable single answer option
90 * to internal code number
91 * @param string name true/false
92 * @return int internal code number
94 public function trans_single($name) {
96 if ($name == "false" || !$name) {
104 * process text string from xml file
105 * @param array $text bit of xml tree after ['text']
106 * @return string processed text.
108 public function import_text($text) {
109 // Quick sanity check.
113 $data = $text[0]['#'];
118 * return the value of a node, given a path to the node
119 * if it doesn't exist return the default value
120 * @param array xml data to read
121 * @param array path path to node expressed as array
122 * @param mixed default
123 * @param bool istext process as text
124 * @param string error if set value must exist, return false and issue message if not
125 * @return mixed value
127 public function getpath($xml, $path, $default, $istext=false, $error='') {
128 foreach ($path as $index) {
129 if (!isset($xml[$index])) {
130 if (!empty($error)) {
131 $this->error($error);
142 if (!is_string($xml)) {
143 $this->error(get_string('invalidxml', 'qformat_xml'));
151 public function import_text_with_files($data, $path, $defaultvalue = '', $defaultformat = 'html') {
153 $field['text'] = $this->getpath($data,
154 array_merge($path, array('#', 'text', 0, '#')), $defaultvalue, true);
155 $field['format'] = $this->trans_format($this->getpath($data,
156 array_merge($path, array('@', 'format')), $defaultformat));
157 $itemid = $this->import_files_as_draft($this->getpath($data,
158 array_merge($path, array('#', 'file')), array(), false));
159 if (!empty($itemid)) {
160 $field['itemid'] = $itemid;
165 public function import_files_as_draft($xml) {
170 $fs = get_file_storage();
171 $itemid = file_get_unused_draft_itemid();
172 $filepaths = array();
173 foreach ($xml as $file) {
174 $filename = $this->getpath($file, array('@', 'name'), '', true);
175 $filepath = $this->getpath($file, array('@', 'path'), '/', true);
176 $fullpath = $filepath . $filename;
177 if (in_array($fullpath, $filepaths)) {
178 debugging('Duplicate file in XML: ' . $fullpath, DEBUG_DEVELOPER);
182 'contextid' => context_user::instance($USER->id)->id,
183 'component' => 'user',
184 'filearea' => 'draft',
186 'filepath' => $filepath,
187 'filename' => $filename,
189 $fs->create_file_from_string($filerecord, base64_decode($file['#']));
190 $filepaths[] = $fullpath;
196 * import parts of question common to all types
197 * @param $question array question question array from xml tree
198 * @return object question object
200 public function import_headers($question) {
203 // This routine initialises the question object.
204 $qo = $this->defaultquestion();
207 $qo->name = $this->clean_question_name($this->getpath($question,
208 array('#', 'name', 0, '#', 'text', 0, '#'), '', true,
209 get_string('xmlimportnoname', 'qformat_xml')));
210 $questiontext = $this->import_text_with_files($question,
211 array('#', 'questiontext', 0));
212 $qo->questiontext = $questiontext['text'];
213 $qo->questiontextformat = $questiontext['format'];
214 if (!empty($questiontext['itemid'])) {
215 $qo->questiontextitemid = $questiontext['itemid'];
217 // Backwards compatibility, deal with the old image tag.
218 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
219 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
220 if ($filedata && $filename) {
221 $fs = get_file_storage();
222 if (empty($qo->questiontextitemid)) {
223 $qo->questiontextitemid = file_get_unused_draft_itemid();
225 $filename = clean_param(str_replace('/', '_', $filename), PARAM_FILE);
227 'contextid' => context_user::instance($USER->id)->id,
228 'component' => 'user',
229 'filearea' => 'draft',
230 'itemid' => $qo->questiontextitemid,
232 'filename' => $filename,
234 $fs->create_file_from_string($filerecord, base64_decode($filedata));
235 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
238 // Restore files in generalfeedback.
239 $generalfeedback = $this->import_text_with_files($question,
240 array('#', 'generalfeedback', 0), $qo->generalfeedback, $this->get_format($qo->questiontextformat));
241 $qo->generalfeedback = $generalfeedback['text'];
242 $qo->generalfeedbackformat = $generalfeedback['format'];
243 if (!empty($generalfeedback['itemid'])) {
244 $qo->generalfeedbackitemid = $generalfeedback['itemid'];
247 $qo->defaultmark = $this->getpath($question,
248 array('#', 'defaultgrade', 0, '#'), $qo->defaultmark);
249 $qo->penalty = $this->getpath($question,
250 array('#', 'penalty', 0, '#'), $qo->penalty);
252 // Fix problematic rounding from old files.
253 if (abs($qo->penalty - 0.3333333) < 0.005) {
254 $qo->penalty = 0.3333333;
257 // Read the question tags.
258 $this->import_question_tags($qo, $question);
264 * Import the common parts of a single answer
265 * @param array answer xml tree for single answer
266 * @param bool $withanswerfiles if true, the answers are HTML (or $defaultformat)
267 * and so may contain files, otherwise the answers are plain text.
268 * @param array Default text format for the feedback, and the answers if $withanswerfiles
270 * @return object answer object
272 public function import_answer($answer, $withanswerfiles = false, $defaultformat = 'html') {
273 $ans = new stdClass();
275 if ($withanswerfiles) {
276 $ans->answer = $this->import_text_with_files($answer, array(), '', $defaultformat);
278 $ans->answer = array();
279 $ans->answer['text'] = $this->getpath($answer, array('#', 'text', 0, '#'), '', true);
280 $ans->answer['format'] = FORMAT_PLAIN;
283 $ans->feedback = $this->import_text_with_files($answer, array('#', 'feedback', 0), '', $defaultformat);
285 $ans->fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
291 * Import the common overall feedback fields.
292 * @param object $question the part of the XML relating to this question.
293 * @param object $qo the question data to add the fields to.
294 * @param bool $withshownumpartscorrect include the shownumcorrect field.
296 public function import_combined_feedback($qo, $questionxml, $withshownumpartscorrect = false) {
297 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
298 foreach ($fields as $field) {
299 $qo->$field = $this->import_text_with_files($questionxml,
300 array('#', $field, 0), '', $this->get_format($qo->questiontextformat));
303 if ($withshownumpartscorrect) {
304 $qo->shownumcorrect = array_key_exists('shownumcorrect', $questionxml['#']);
306 // Backwards compatibility.
307 if (array_key_exists('correctresponsesfeedback', $questionxml['#'])) {
308 $qo->shownumcorrect = $this->trans_single($this->getpath($questionxml,
309 array('#', 'correctresponsesfeedback', 0, '#'), 1));
315 * Import a question hint
316 * @param array $hintxml hint xml fragment.
317 * @param string $defaultformat the text format to assume for hints that do not specify.
318 * @return object hint for storing in the database.
320 public function import_hint($hintxml, $defaultformat) {
321 $hint = new stdClass();
322 if (array_key_exists('hintcontent', $hintxml['#'])) {
323 // Backwards compatibility.
325 $hint->hint = $this->import_text_with_files($hintxml,
326 array('#', 'hintcontent', 0), '', $defaultformat);
328 $hint->shownumcorrect = $this->getpath($hintxml,
329 array('#', 'statenumberofcorrectresponses', 0, '#'), 0);
330 $hint->clearwrong = $this->getpath($hintxml,
331 array('#', 'clearincorrectresponses', 0, '#'), 0);
332 $hint->options = $this->getpath($hintxml,
333 array('#', 'showfeedbacktoresponses', 0, '#'), 0);
337 $hint->hint = $this->import_text_with_files($hintxml, array(), '', $defaultformat);
338 $hint->shownumcorrect = array_key_exists('shownumcorrect', $hintxml['#']);
339 $hint->clearwrong = array_key_exists('clearwrong', $hintxml['#']);
340 $hint->options = $this->getpath($hintxml, array('#', 'options', 0, '#'), '', true);
346 * Import all the question hints
348 * @param object $qo the question data that is being constructed.
349 * @param array $questionxml The xml representing the question.
350 * @param bool $withparts whether the extra fields relating to parts should be imported.
351 * @param bool $withoptions whether the extra options field should be imported.
352 * @param string $defaultformat the text format to assume for hints that do not specify.
353 * @return array of objects representing the hints in the file.
355 public function import_hints($qo, $questionxml, $withparts = false,
356 $withoptions = false, $defaultformat = 'html') {
357 if (!isset($questionxml['#']['hint'])) {
361 foreach ($questionxml['#']['hint'] as $hintxml) {
362 $hint = $this->import_hint($hintxml, $defaultformat);
363 $qo->hint[] = $hint->hint;
366 $qo->hintshownumcorrect[] = $hint->shownumcorrect;
367 $qo->hintclearwrong[] = $hint->clearwrong;
371 $qo->hintoptions[] = $hint->options;
377 * Import all the question tags
379 * @param object $qo the question data that is being constructed.
380 * @param array $questionxml The xml representing the question.
381 * @return array of objects representing the tags in the file.
383 public function import_question_tags($qo, $questionxml) {
386 if (!empty($CFG->usetags) && array_key_exists('tags', $questionxml['#'])
387 && !empty($questionxml['#']['tags'][0]['#']['tag'])) {
388 require_once($CFG->dirroot.'/tag/lib.php');
390 foreach ($questionxml['#']['tags'][0]['#']['tag'] as $tagdata) {
391 $qo->tags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true);
397 * Import files from a node in the XML.
398 * @param array $xml an array of <file> nodes from the the parsed XML.
399 * @return array of things representing files - in the form that save_question expects.
401 public function import_files($xml) {
403 foreach ($xml as $file) {
404 $data = new stdClass();
405 $data->content = $file['#'];
406 $data->encoding = $file['@']['encoding'];
407 $data->name = $file['@']['name'];
414 * import multiple choice question
415 * @param array question question array from xml tree
416 * @return object question object
418 public function import_multichoice($question) {
420 $qo = $this->import_headers($question);
422 // Header parts particular to multichoice.
423 $qo->qtype = 'multichoice';
424 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
425 $qo->single = $this->trans_single($single);
426 $shuffleanswers = $this->getpath($question,
427 array('#', 'shuffleanswers', 0, '#'), 'false');
428 $qo->answernumbering = $this->getpath($question,
429 array('#', 'answernumbering', 0, '#'), 'abc');
430 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
432 // There was a time on the 1.8 branch when it could output an empty
433 // answernumbering tag, so fix up any found.
434 if (empty($qo->answernumbering)) {
435 $qo->answernumbering = 'abc';
438 // Run through the answers.
439 $answers = $question['#']['answer'];
441 foreach ($answers as $answer) {
442 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
443 $qo->answer[$acount] = $ans->answer;
444 $qo->fraction[$acount] = $ans->fraction;
445 $qo->feedback[$acount] = $ans->feedback;
449 $this->import_combined_feedback($qo, $question, true);
450 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
456 * Import cloze type question
457 * @param array question question array from xml tree
458 * @return object question object
460 public function import_multianswer($question) {
462 question_bank::get_qtype('multianswer');
464 $questiontext = $this->import_text_with_files($question,
465 array('#', 'questiontext', 0));
466 $qo = qtype_multianswer_extract_question($questiontext);
468 // Header parts particular to multianswer.
469 $qo->qtype = 'multianswer';
471 // Only set the course if the data is available.
472 if (isset($this->course)) {
473 $qo->course = $this->course;
476 $qo->name = $this->clean_question_name($this->import_text($question['#']['name'][0]['#']['text']));
477 $qo->questiontextformat = $questiontext['format'];
478 $qo->questiontext = $qo->questiontext['text'];
479 if (!empty($questiontext['itemid'])) {
480 $qo->questiontextitemid = $questiontext['itemid'];
483 // Backwards compatibility, deal with the old image tag.
484 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
485 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
486 if ($filedata && $filename) {
487 $fs = get_file_storage();
488 if (empty($qo->questiontextitemid)) {
489 $qo->questiontextitemid = file_get_unused_draft_itemid();
491 $filename = clean_param(str_replace('/', '_', $filename), PARAM_FILE);
493 'contextid' => context_user::instance($USER->id)->id,
494 'component' => 'user',
495 'filearea' => 'draft',
496 'itemid' => $qo->questiontextitemid,
498 'filename' => $filename,
500 $fs->create_file_from_string($filerecord, base64_decode($filedata));
501 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
504 // Restore files in generalfeedback.
505 $generalfeedback = $this->import_text_with_files($question,
506 array('#', 'generalfeedback', 0), $qo->generalfeedback, $this->get_format($qo->questiontextformat));
507 $qo->generalfeedback = $generalfeedback['text'];
508 $qo->generalfeedbackformat = $generalfeedback['format'];
509 if (!empty($generalfeedback['itemid'])) {
510 $qo->generalfeedbackitemid = $generalfeedback['itemid'];
513 $qo->penalty = $this->getpath($question,
514 array('#', 'penalty', 0, '#'), $this->defaultquestion()->penalty);
515 // Fix problematic rounding from old files.
516 if (abs($qo->penalty - 0.3333333) < 0.005) {
517 $qo->penalty = 0.3333333;
520 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
521 $this->import_question_tags($qo, $question);
527 * Import true/false type question
528 * @param array question question array from xml tree
529 * @return object question object
531 public function import_truefalse($question) {
534 $qo = $this->import_headers($question);
536 // Header parts particular to true/false.
537 $qo->qtype = 'truefalse';
539 // In the past, it used to be assumed that the two answers were in the file
540 // true first, then false. Howevever that was not always true. Now, we
541 // try to match on the answer text, but in old exports, this will be a localised
542 // string, so if we don't find true or false, we fall back to the old system.
545 foreach ($question['#']['answer'] as $answer) {
546 $answertext = $this->getpath($answer,
547 array('#', 'text', 0, '#'), '', true);
548 $feedback = $this->import_text_with_files($answer,
549 array('#', 'feedback', 0), '', $this->get_format($qo->questiontextformat));
551 if ($answertext != 'true' && $answertext != 'false') {
552 // Old style file, assume order is true/false.
555 $answertext = 'true';
557 $answertext = 'false';
561 if ($answertext == 'true') {
562 $qo->answer = ($answer['@']['fraction'] == 100);
563 $qo->correctanswer = $qo->answer;
564 $qo->feedbacktrue = $feedback;
566 $qo->answer = ($answer['@']['fraction'] != 100);
567 $qo->correctanswer = $qo->answer;
568 $qo->feedbackfalse = $feedback;
575 $a->questiontext = $qo->questiontext;
576 $a->answer = get_string($qo->correctanswer ? 'true' : 'false', 'qtype_truefalse');
577 echo $OUTPUT->notification(get_string('truefalseimporterror', 'qformat_xml', $a));
580 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
586 * Import short answer type question
587 * @param array question question array from xml tree
588 * @return object question object
590 public function import_shortanswer($question) {
592 $qo = $this->import_headers($question);
594 // Header parts particular to shortanswer.
595 $qo->qtype = 'shortanswer';
598 $qo->usecase = $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase);
600 // Run through the answers.
601 $answers = $question['#']['answer'];
603 foreach ($answers as $answer) {
604 $ans = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
605 $qo->answer[$acount] = $ans->answer['text'];
606 $qo->fraction[$acount] = $ans->fraction;
607 $qo->feedback[$acount] = $ans->feedback;
611 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
617 * Import description type question
618 * @param array question question array from xml tree
619 * @return object question object
621 public function import_description($question) {
623 $qo = $this->import_headers($question);
624 // Header parts particular to shortanswer.
625 $qo->qtype = 'description';
626 $qo->defaultmark = 0;
632 * Import numerical type question
633 * @param array question question array from xml tree
634 * @return object question object
636 public function import_numerical($question) {
638 $qo = $this->import_headers($question);
640 // Header parts particular to numerical.
641 $qo->qtype = 'numerical';
643 // Get answers array.
644 $answers = $question['#']['answer'];
645 $qo->answer = array();
646 $qo->feedback = array();
647 $qo->fraction = array();
648 $qo->tolerance = array();
649 foreach ($answers as $answer) {
650 // Answer outside of <text> is deprecated.
651 $obj = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
652 $qo->answer[] = $obj->answer['text'];
653 if (empty($qo->answer)) {
656 $qo->feedback[] = $obj->feedback;
657 $qo->tolerance[] = $this->getpath($answer, array('#', 'tolerance', 0, '#'), 0);
659 // Fraction as a tag is deprecated.
660 $fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
661 $qo->fraction[] = $this->getpath($answer,
662 array('#', 'fraction', 0, '#'), $fraction); // Deprecated.
665 // Get the units array.
667 $units = $this->getpath($question, array('#', 'units', 0, '#', 'unit'), array());
668 if (!empty($units)) {
669 $qo->multiplier = array();
670 foreach ($units as $unit) {
671 $qo->multiplier[] = $this->getpath($unit, array('#', 'multiplier', 0, '#'), 1);
672 $qo->unit[] = $this->getpath($unit, array('#', 'unit_name', 0, '#'), '', true);
675 $qo->unitgradingtype = $this->getpath($question, array('#', 'unitgradingtype', 0, '#'), 0);
676 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0.1);
677 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), null);
678 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
679 $qo->instructions['text'] = '';
680 $qo->instructions['format'] = FORMAT_HTML;
681 $instructions = $this->getpath($question, array('#', 'instructions'), array());
682 if (!empty($instructions)) {
683 $qo->instructions = $this->import_text_with_files($instructions,
684 array('0'), '', $this->get_format($qo->questiontextformat));
687 if (is_null($qo->showunits)) {
688 // Set a good default, depending on whether there are any units defined.
689 if (empty($qo->unit)) {
690 $qo->showunits = 3; // This is qtype_numerical::UNITNONE, but we cannot refer to that constant here.
692 $qo->showunits = 0; // This is qtype_numerical::UNITOPTIONAL, but we cannot refer to that constant here.
696 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
702 * Import matching type question
703 * @param array question question array from xml tree
704 * @return object question object
706 public function import_match($question) {
708 $qo = $this->import_headers($question);
710 // Header parts particular to matching.
711 $qo->qtype = 'match';
712 $qo->shuffleanswers = $this->trans_single($this->getpath($question,
713 array('#', 'shuffleanswers', 0, '#'), 1));
715 // Run through subquestions.
716 $qo->subquestions = array();
717 $qo->subanswers = array();
718 foreach ($question['#']['subquestion'] as $subqxml) {
719 $qo->subquestions[] = $this->import_text_with_files($subqxml,
720 array(), '', $this->get_format($qo->questiontextformat));
722 $answers = $this->getpath($subqxml, array('#', 'answer'), array());
723 $qo->subanswers[] = $this->getpath($subqxml,
724 array('#', 'answer', 0, '#', 'text', 0, '#'), '', true);
727 $this->import_combined_feedback($qo, $question, true);
728 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
734 * Import essay type question
735 * @param array question question array from xml tree
736 * @return object question object
738 public function import_essay($question) {
740 $qo = $this->import_headers($question);
742 // Header parts particular to essay.
743 $qo->qtype = 'essay';
745 $qo->responseformat = $this->getpath($question,
746 array('#', 'responseformat', 0, '#'), 'editor');
747 $qo->responsefieldlines = $this->getpath($question,
748 array('#', 'responsefieldlines', 0, '#'), 15);
749 $qo->responserequired = $this->getpath($question,
750 array('#', 'responserequired', 0, '#'), 1);
751 $qo->attachments = $this->getpath($question,
752 array('#', 'attachments', 0, '#'), 0);
753 $qo->attachmentsrequired = $this->getpath($question,
754 array('#', 'attachmentsrequired', 0, '#'), 0);
755 $qo->graderinfo = $this->import_text_with_files($question,
756 array('#', 'graderinfo', 0), '', $this->get_format($qo->questiontextformat));
757 $qo->responsetemplate['text'] = $this->getpath($question,
758 array('#', 'responsetemplate', 0, '#', 'text', 0, '#'), '', true);
759 $qo->responsetemplate['format'] = $this->trans_format($this->getpath($question,
760 array('#', 'responsetemplate', 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
766 * Import a calculated question
767 * @param object $question the imported XML data.
769 public function import_calculated($question) {
772 $qo = $this->import_headers($question);
774 // Header parts particular to calculated.
775 $qo->qtype = 'calculated';
776 $qo->synchronize = $this->getpath($question, array('#', 'synchronize', 0, '#'), 0);
777 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
778 $qo->single = $this->trans_single($single);
779 $shuffleanswers = $this->getpath($question, array('#', 'shuffleanswers', 0, '#'), 'false');
780 $qo->answernumbering = $this->getpath($question,
781 array('#', 'answernumbering', 0, '#'), 'abc');
782 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
784 $this->import_combined_feedback($qo, $question);
786 $qo->unitgradingtype = $this->getpath($question,
787 array('#', 'unitgradingtype', 0, '#'), 0);
788 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), null);
789 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
790 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
791 $qo->instructions = $this->getpath($question,
792 array('#', 'instructions', 0, '#', 'text', 0, '#'), '', true);
793 if (!empty($instructions)) {
794 $qo->instructions = $this->import_text_with_files($instructions,
795 array('0'), '', $this->get_format($qo->questiontextformat));
798 // Get answers array.
799 $answers = $question['#']['answer'];
800 $qo->answer = array();
801 $qo->feedback = array();
802 $qo->fraction = array();
803 $qo->tolerance = array();
804 $qo->tolerancetype = array();
805 $qo->correctanswerformat = array();
806 $qo->correctanswerlength = array();
807 $qo->feedback = array();
808 foreach ($answers as $answer) {
809 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
810 // Answer outside of <text> is deprecated.
811 if (empty($ans->answer['text'])) {
812 $ans->answer['text'] = '*';
814 $qo->answer[] = $ans->answer['text'];
815 $qo->feedback[] = $ans->feedback;
816 $qo->tolerance[] = $answer['#']['tolerance'][0]['#'];
817 // Fraction as a tag is deprecated.
818 if (!empty($answer['#']['fraction'][0]['#'])) {
819 $qo->fraction[] = $answer['#']['fraction'][0]['#'];
821 $qo->fraction[] = $answer['@']['fraction'] / 100;
823 $qo->tolerancetype[] = $answer['#']['tolerancetype'][0]['#'];
824 $qo->correctanswerformat[] = $answer['#']['correctanswerformat'][0]['#'];
825 $qo->correctanswerlength[] = $answer['#']['correctanswerlength'][0]['#'];
829 if (isset($question['#']['units'][0]['#']['unit'])) {
830 $units = $question['#']['units'][0]['#']['unit'];
831 $qo->multiplier = array();
832 foreach ($units as $unit) {
833 $qo->multiplier[] = $unit['#']['multiplier'][0]['#'];
834 $qo->unit[] = $unit['#']['unit_name'][0]['#'];
837 $instructions = $this->getpath($question, array('#', 'instructions'), array());
838 if (!empty($instructions)) {
839 $qo->instructions = $this->import_text_with_files($instructions,
840 array('0'), '', $this->get_format($qo->questiontextformat));
843 if (is_null($qo->unitpenalty)) {
844 // Set a good default, depending on whether there are any units defined.
845 if (empty($qo->unit)) {
846 $qo->showunits = 3; // This is qtype_numerical::UNITNONE, but we cannot refer to that constant here.
848 $qo->showunits = 0; // This is qtype_numerical::UNITOPTIONAL, but we cannot refer to that constant here.
852 $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition'];
853 $qo->dataset = array();
854 $qo->datasetindex= 0;
855 foreach ($datasets as $dataset) {
857 $qo->dataset[$qo->datasetindex] = new stdClass();
858 $qo->dataset[$qo->datasetindex]->status =
859 $this->import_text($dataset['#']['status'][0]['#']['text']);
860 $qo->dataset[$qo->datasetindex]->name =
861 $this->import_text($dataset['#']['name'][0]['#']['text']);
862 $qo->dataset[$qo->datasetindex]->type =
863 $dataset['#']['type'][0]['#'];
864 $qo->dataset[$qo->datasetindex]->distribution =
865 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
866 $qo->dataset[$qo->datasetindex]->max =
867 $this->import_text($dataset['#']['maximum'][0]['#']['text']);
868 $qo->dataset[$qo->datasetindex]->min =
869 $this->import_text($dataset['#']['minimum'][0]['#']['text']);
870 $qo->dataset[$qo->datasetindex]->length =
871 $this->import_text($dataset['#']['decimals'][0]['#']['text']);
872 $qo->dataset[$qo->datasetindex]->distribution =
873 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
874 $qo->dataset[$qo->datasetindex]->itemcount = $dataset['#']['itemcount'][0]['#'];
875 $qo->dataset[$qo->datasetindex]->datasetitem = array();
876 $qo->dataset[$qo->datasetindex]->itemindex = 0;
877 $qo->dataset[$qo->datasetindex]->number_of_items = $this->getpath($dataset,
878 array('#', 'number_of_items', 0, '#'), 0);
879 $datasetitems = $this->getpath($dataset,
880 array('#', 'dataset_items', 0, '#', 'dataset_item'), array());
881 foreach ($datasetitems as $datasetitem) {
882 $qo->dataset[$qo->datasetindex]->itemindex++;
883 $qo->dataset[$qo->datasetindex]->datasetitem[
884 $qo->dataset[$qo->datasetindex]->itemindex] = new stdClass();
885 $qo->dataset[$qo->datasetindex]->datasetitem[
886 $qo->dataset[$qo->datasetindex]->itemindex]->itemnumber =
887 $datasetitem['#']['number'][0]['#'];
888 $qo->dataset[$qo->datasetindex]->datasetitem[
889 $qo->dataset[$qo->datasetindex]->itemindex]->value =
890 $datasetitem['#']['value'][0]['#'];
894 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
900 * This is not a real question type. It's a dummy type used to specify the
901 * import category. The format is:
902 * <question type="category">
903 * <category>tom/dick/harry</category>
906 protected function import_category($question) {
907 $qo = new stdClass();
908 $qo->qtype = 'category';
909 $qo->category = $this->import_text($question['#']['category'][0]['#']['text']);
914 * Parse the array of lines into an array of questions
915 * this *could* burn memory - but it won't happen that much
916 * so fingers crossed!
917 * @param array of lines from the input file.
918 * @param stdClass $context
919 * @return array (of objects) question objects.
921 protected function readquestions($lines) {
922 // We just need it as one big string.
923 $lines = implode('', $lines);
925 // This converts xml to big nasty data structure
926 // the 0 means keep white space as it is (important for markdown format).
928 $xml = xmlize($lines, 0, 'UTF-8', true);
929 } catch (xml_format_exception $e) {
930 $this->error($e->getMessage(), '');
933 unset($lines); // No need to keep this in memory.
934 return $this->import_questions($xml['quiz']['#']['question']);
938 * @param array $xml the xmlized xml
939 * @return stdClass[] question objects to pass to question type save_question_options
941 public function import_questions($xml) {
942 $questions = array();
944 // Iterate through questions.
945 foreach ($xml as $questionxml) {
946 $qo = $this->import_question($questionxml);
948 // Stick the result in the $questions array.
957 * @param array $questionxml xml describing the question
958 * @return null|stdClass an object with data to be fed to question type save_question_options
960 protected function import_question($questionxml) {
961 $questiontype = $questionxml['@']['type'];
963 if ($questiontype == 'multichoice') {
964 return $this->import_multichoice($questionxml);
965 } else if ($questiontype == 'truefalse') {
966 return $this->import_truefalse($questionxml);
967 } else if ($questiontype == 'shortanswer') {
968 return $this->import_shortanswer($questionxml);
969 } else if ($questiontype == 'numerical') {
970 return $this->import_numerical($questionxml);
971 } else if ($questiontype == 'description') {
972 return $this->import_description($questionxml);
973 } else if ($questiontype == 'matching' || $questiontype == 'match') {
974 return $this->import_match($questionxml);
975 } else if ($questiontype == 'cloze' || $questiontype == 'multianswer') {
976 return $this->import_multianswer($questionxml);
977 } else if ($questiontype == 'essay') {
978 return $this->import_essay($questionxml);
979 } else if ($questiontype == 'calculated') {
980 return $this->import_calculated($questionxml);
981 } else if ($questiontype == 'calculatedsimple') {
982 $qo = $this->import_calculated($questionxml);
983 $qo->qtype = 'calculatedsimple';
985 } else if ($questiontype == 'calculatedmulti') {
986 $qo = $this->import_calculated($questionxml);
987 $qo->qtype = 'calculatedmulti';
989 } else if ($questiontype == 'category') {
990 return $this->import_category($questionxml);
993 // Not a type we handle ourselves. See if the question type wants
995 if (!$qo = $this->try_importing_using_qtypes($questionxml, null, null, $questiontype)) {
996 $this->error(get_string('xmltypeunsupported', 'qformat_xml', $questiontype));
1003 // EXPORT FUNCTIONS START HERE.
1005 public function export_file_extension() {
1010 * Turn the internal question type name into a human readable form.
1011 * (In the past, the code used to use integers internally. Now, it uses
1012 * strings, so there is less need for this, but to maintain
1013 * backwards-compatibility we change two of the type names.)
1014 * @param string $qtype question type plugin name.
1015 * @return string $qtype string to use in the file.
1017 protected function get_qtype($qtype) {
1029 * Convert internal Moodle text format code into
1030 * human readable form
1031 * @param int id internal code
1032 * @return string format text
1034 public function get_format($id) {
1037 return 'moodle_auto_format';
1041 return 'plain_text';
1044 case FORMAT_MARKDOWN:
1052 * Convert internal single question code into
1053 * human readable form
1054 * @param int id single question code
1055 * @return string single question string
1057 public function get_single($id) {
1069 * Take a string, and wrap it in a CDATA secion, if that is required to make
1070 * the output XML valid.
1071 * @param string $string a string
1072 * @return string the string, wrapped in CDATA if necessary.
1074 public function xml_escape($string) {
1075 if (!empty($string) && htmlspecialchars($string) != $string) {
1076 // If the string contains something that looks like the end
1077 // of a CDATA section, then we need to avoid errors by splitting
1078 // the string between two CDATA sections.
1079 $string = str_replace(']]>', ']]]]><![CDATA[>', $string);
1080 return "<![CDATA[{$string}]]>";
1087 * Generates <text></text> tags, processing raw text therein
1088 * @param string $raw the content to output.
1089 * @param int $indent the current indent level.
1090 * @param bool $short stick it on one line.
1091 * @return string formatted text.
1093 public function writetext($raw, $indent = 0, $short = true) {
1094 $indent = str_repeat(' ', $indent);
1095 $raw = $this->xml_escape($raw);
1098 $xml = "{$indent}<text>{$raw}</text>\n";
1100 $xml = "{$indent}<text>\n{$raw}\n{$indent}</text>\n";
1107 * Generte the XML to represent some files.
1108 * @param array of store array of stored_file objects.
1109 * @return string $string the XML.
1111 public function write_files($files) {
1112 if (empty($files)) {
1116 foreach ($files as $file) {
1117 if ($file->is_directory()) {
1120 $string .= '<file name="' . $file->get_filename() . '" path="' . $file->get_filepath() . '" encoding="base64">';
1121 $string .= base64_encode($file->get_content());
1122 $string .= "</file>\n";
1127 protected function presave_process($content) {
1128 // Override to allow us to add xml headers and footers.
1129 return '<?xml version="1.0" encoding="UTF-8"?>
1131 ' . $content . '</quiz>';
1135 * Turns question into an xml segment
1136 * @param object $question the question data.
1137 * @return string xml segment
1139 public function writequestion($question) {
1140 global $CFG, $OUTPUT;
1142 $invalidquestion = false;
1143 $fs = get_file_storage();
1144 $contextid = $question->contextid;
1145 // Get files used by the questiontext.
1146 $question->questiontextfiles = $fs->get_area_files(
1147 $contextid, 'question', 'questiontext', $question->id);
1148 // Get files used by the generalfeedback.
1149 $question->generalfeedbackfiles = $fs->get_area_files(
1150 $contextid, 'question', 'generalfeedback', $question->id);
1151 if (!empty($question->options->answers)) {
1152 foreach ($question->options->answers as $answer) {
1153 $answer->answerfiles = $fs->get_area_files(
1154 $contextid, 'question', 'answer', $answer->id);
1155 $answer->feedbackfiles = $fs->get_area_files(
1156 $contextid, 'question', 'answerfeedback', $answer->id);
1162 // Add a comment linking this to the original question id.
1163 $expout .= "<!-- question: {$question->id} -->\n";
1165 // Check question type.
1166 $questiontype = $this->get_qtype($question->qtype);
1168 // Categories are a special case.
1169 if ($question->qtype == 'category') {
1170 $categorypath = $this->writetext($question->category);
1171 $expout .= " <question type=\"category\">\n";
1172 $expout .= " <category>\n";
1173 $expout .= " {$categorypath}\n";
1174 $expout .= " </category>\n";
1175 $expout .= " </question>\n";
1179 // Now we know we are are handing a real question.
1180 // Output the generic information.
1181 $expout .= " <question type=\"{$questiontype}\">\n";
1182 $expout .= " <name>\n";
1183 $expout .= $this->writetext($question->name, 3);
1184 $expout .= " </name>\n";
1185 $expout .= " <questiontext {$this->format($question->questiontextformat)}>\n";
1186 $expout .= $this->writetext($question->questiontext, 3);
1187 $expout .= $this->write_files($question->questiontextfiles);
1188 $expout .= " </questiontext>\n";
1189 $expout .= " <generalfeedback {$this->format($question->generalfeedbackformat)}>\n";
1190 $expout .= $this->writetext($question->generalfeedback, 3);
1191 $expout .= $this->write_files($question->generalfeedbackfiles);
1192 $expout .= " </generalfeedback>\n";
1193 if ($question->qtype != 'multianswer') {
1194 $expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
1196 $expout .= " <penalty>{$question->penalty}</penalty>\n";
1197 $expout .= " <hidden>{$question->hidden}</hidden>\n";
1199 // The rest of the output depends on question type.
1200 switch($question->qtype) {
1202 // Not a qtype really - dummy used for category switching.
1206 $trueanswer = $question->options->answers[$question->options->trueanswer];
1207 $trueanswer->answer = 'true';
1208 $expout .= $this->write_answer($trueanswer);
1210 $falseanswer = $question->options->answers[$question->options->falseanswer];
1211 $falseanswer->answer = 'false';
1212 $expout .= $this->write_answer($falseanswer);
1216 $expout .= " <single>" . $this->get_single($question->options->single) .
1218 $expout .= " <shuffleanswers>" .
1219 $this->get_single($question->options->shuffleanswers) .
1220 "</shuffleanswers>\n";
1221 $expout .= " <answernumbering>" . $question->options->answernumbering .
1222 "</answernumbering>\n";
1223 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
1224 $expout .= $this->write_answers($question->options->answers);
1228 $expout .= " <usecase>{$question->options->usecase}</usecase>\n";
1229 $expout .= $this->write_answers($question->options->answers);
1233 foreach ($question->options->answers as $answer) {
1234 $expout .= $this->write_answer($answer,
1235 " <tolerance>{$answer->tolerance}</tolerance>\n");
1238 $units = $question->options->units;
1239 if (count($units)) {
1240 $expout .= "<units>\n";
1241 foreach ($units as $unit) {
1242 $expout .= " <unit>\n";
1243 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1244 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1245 $expout .= " </unit>\n";
1247 $expout .= "</units>\n";
1249 if (isset($question->options->unitgradingtype)) {
1250 $expout .= " <unitgradingtype>" . $question->options->unitgradingtype .
1251 "</unitgradingtype>\n";
1253 if (isset($question->options->unitpenalty)) {
1254 $expout .= " <unitpenalty>{$question->options->unitpenalty}</unitpenalty>\n";
1256 if (isset($question->options->showunits)) {
1257 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1259 if (isset($question->options->unitsleft)) {
1260 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1262 if (!empty($question->options->instructionsformat)) {
1263 $files = $fs->get_area_files($contextid, 'qtype_numerical',
1264 'instruction', $question->id);
1265 $expout .= " <instructions " .
1266 $this->format($question->options->instructionsformat) . ">\n";
1267 $expout .= $this->writetext($question->options->instructions, 3);
1268 $expout .= $this->write_files($files);
1269 $expout .= " </instructions>\n";
1274 $expout .= " <shuffleanswers>" .
1275 $this->get_single($question->options->shuffleanswers) .
1276 "</shuffleanswers>\n";
1277 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
1278 foreach ($question->options->subquestions as $subquestion) {
1279 $files = $fs->get_area_files($contextid, 'qtype_match',
1280 'subquestion', $subquestion->id);
1281 $expout .= " <subquestion " .
1282 $this->format($subquestion->questiontextformat) . ">\n";
1283 $expout .= $this->writetext($subquestion->questiontext, 3);
1284 $expout .= $this->write_files($files);
1285 $expout .= " <answer>\n";
1286 $expout .= $this->writetext($subquestion->answertext, 4);
1287 $expout .= " </answer>\n";
1288 $expout .= " </subquestion>\n";
1293 // Nothing else to do.
1297 foreach ($question->options->questions as $index => $subq) {
1298 $expout = str_replace('{#' . $index . '}', $subq->questiontext, $expout);
1303 $expout .= " <responseformat>" . $question->options->responseformat .
1304 "</responseformat>\n";
1305 $expout .= " <responserequired>" . $question->options->responserequired .
1306 "</responserequired>\n";
1307 $expout .= " <responsefieldlines>" . $question->options->responsefieldlines .
1308 "</responsefieldlines>\n";
1309 $expout .= " <attachments>" . $question->options->attachments .
1311 $expout .= " <attachmentsrequired>" . $question->options->attachmentsrequired .
1312 "</attachmentsrequired>\n";
1313 $expout .= " <graderinfo " .
1314 $this->format($question->options->graderinfoformat) . ">\n";
1315 $expout .= $this->writetext($question->options->graderinfo, 3);
1316 $expout .= $this->write_files($fs->get_area_files($contextid, 'qtype_essay',
1317 'graderinfo', $question->id));
1318 $expout .= " </graderinfo>\n";
1319 $expout .= " <responsetemplate " .
1320 $this->format($question->options->responsetemplateformat) . ">\n";
1321 $expout .= $this->writetext($question->options->responsetemplate, 3);
1322 $expout .= " </responsetemplate>\n";
1326 case 'calculatedsimple':
1327 case 'calculatedmulti':
1328 $expout .= " <synchronize>{$question->options->synchronize}</synchronize>\n";
1329 $expout .= " <single>{$question->options->single}</single>\n";
1330 $expout .= " <answernumbering>" . $question->options->answernumbering .
1331 "</answernumbering>\n";
1332 $expout .= " <shuffleanswers>" . $question->options->shuffleanswers .
1333 "</shuffleanswers>\n";
1335 $component = 'qtype_' . $question->qtype;
1336 $files = $fs->get_area_files($contextid, $component,
1337 'correctfeedback', $question->id);
1338 $expout .= " <correctfeedback>\n";
1339 $expout .= $this->writetext($question->options->correctfeedback, 3);
1340 $expout .= $this->write_files($files);
1341 $expout .= " </correctfeedback>\n";
1343 $files = $fs->get_area_files($contextid, $component,
1344 'partiallycorrectfeedback', $question->id);
1345 $expout .= " <partiallycorrectfeedback>\n";
1346 $expout .= $this->writetext($question->options->partiallycorrectfeedback, 3);
1347 $expout .= $this->write_files($files);
1348 $expout .= " </partiallycorrectfeedback>\n";
1350 $files = $fs->get_area_files($contextid, $component,
1351 'incorrectfeedback', $question->id);
1352 $expout .= " <incorrectfeedback>\n";
1353 $expout .= $this->writetext($question->options->incorrectfeedback, 3);
1354 $expout .= $this->write_files($files);
1355 $expout .= " </incorrectfeedback>\n";
1357 foreach ($question->options->answers as $answer) {
1358 $percent = 100 * $answer->fraction;
1359 $expout .= "<answer fraction=\"{$percent}\">\n";
1360 // The "<text/>" tags are an added feature, old files won't have them.
1361 $expout .= " <text>{$answer->answer}</text>\n";
1362 $expout .= " <tolerance>{$answer->tolerance}</tolerance>\n";
1363 $expout .= " <tolerancetype>{$answer->tolerancetype}</tolerancetype>\n";
1364 $expout .= " <correctanswerformat>" .
1365 $answer->correctanswerformat . "</correctanswerformat>\n";
1366 $expout .= " <correctanswerlength>" .
1367 $answer->correctanswerlength . "</correctanswerlength>\n";
1368 $expout .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1369 $files = $fs->get_area_files($contextid, $component,
1370 'instruction', $question->id);
1371 $expout .= $this->writetext($answer->feedback);
1372 $expout .= $this->write_files($answer->feedbackfiles);
1373 $expout .= " </feedback>\n";
1374 $expout .= "</answer>\n";
1376 if (isset($question->options->unitgradingtype)) {
1377 $expout .= " <unitgradingtype>" .
1378 $question->options->unitgradingtype . "</unitgradingtype>\n";
1380 if (isset($question->options->unitpenalty)) {
1381 $expout .= " <unitpenalty>" .
1382 $question->options->unitpenalty . "</unitpenalty>\n";
1384 if (isset($question->options->showunits)) {
1385 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1387 if (isset($question->options->unitsleft)) {
1388 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1391 if (isset($question->options->instructionsformat)) {
1392 $files = $fs->get_area_files($contextid, $component,
1393 'instruction', $question->id);
1394 $expout .= " <instructions " .
1395 $this->format($question->options->instructionsformat) . ">\n";
1396 $expout .= $this->writetext($question->options->instructions, 3);
1397 $expout .= $this->write_files($files);
1398 $expout .= " </instructions>\n";
1401 if (isset($question->options->units)) {
1402 $units = $question->options->units;
1403 if (count($units)) {
1404 $expout .= "<units>\n";
1405 foreach ($units as $unit) {
1406 $expout .= " <unit>\n";
1407 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1408 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1409 $expout .= " </unit>\n";
1411 $expout .= "</units>\n";
1415 // The tag $question->export_process has been set so we get all the
1416 // data items in the database from the function
1417 // qtype_calculated::get_question_options calculatedsimple defaults
1419 if (isset($question->options->datasets) && count($question->options->datasets)) {
1420 $expout .= "<dataset_definitions>\n";
1421 foreach ($question->options->datasets as $def) {
1422 $expout .= "<dataset_definition>\n";
1423 $expout .= " <status>".$this->writetext($def->status)."</status>\n";
1424 $expout .= " <name>".$this->writetext($def->name)."</name>\n";
1425 if ($question->qtype == 'calculated') {
1426 $expout .= " <type>calculated</type>\n";
1428 $expout .= " <type>calculatedsimple</type>\n";
1430 $expout .= " <distribution>" . $this->writetext($def->distribution) .
1431 "</distribution>\n";
1432 $expout .= " <minimum>" . $this->writetext($def->minimum) .
1434 $expout .= " <maximum>" . $this->writetext($def->maximum) .
1436 $expout .= " <decimals>" . $this->writetext($def->decimals) .
1438 $expout .= " <itemcount>{$def->itemcount}</itemcount>\n";
1439 if ($def->itemcount > 0) {
1440 $expout .= " <dataset_items>\n";
1441 foreach ($def->items as $item) {
1442 $expout .= " <dataset_item>\n";
1443 $expout .= " <number>".$item->itemnumber."</number>\n";
1444 $expout .= " <value>".$item->value."</value>\n";
1445 $expout .= " </dataset_item>\n";
1447 $expout .= " </dataset_items>\n";
1448 $expout .= " <number_of_items>" . $def->number_of_items .
1449 "</number_of_items>\n";
1451 $expout .= "</dataset_definition>\n";
1453 $expout .= "</dataset_definitions>\n";
1458 // Try support by optional plugin.
1459 if (!$data = $this->try_exporting_using_qtypes($question->qtype, $question)) {
1460 $invalidquestion = true;
1466 // Output any hints.
1467 $expout .= $this->write_hints($question);
1469 // Write the question tags.
1470 if (!empty($CFG->usetags)) {
1471 require_once($CFG->dirroot.'/tag/lib.php');
1472 $tags = tag_get_tags_array('question', $question->id);
1473 if (!empty($tags)) {
1474 $expout .= " <tags>\n";
1475 foreach ($tags as $tag) {
1476 $expout .= " <tag>" . $this->writetext($tag, 0, true) . "</tag>\n";
1478 $expout .= " </tags>\n";
1482 // Close the question tag.
1483 $expout .= " </question>\n";
1484 if ($invalidquestion) {
1491 public function write_answers($answers) {
1492 if (empty($answers)) {
1496 foreach ($answers as $answer) {
1497 $output .= $this->write_answer($answer);
1502 public function write_answer($answer, $extra = '') {
1503 $percent = $answer->fraction * 100;
1505 $output .= " <answer fraction=\"{$percent}\" {$this->format($answer->answerformat)}>\n";
1506 $output .= $this->writetext($answer->answer, 3);
1507 $output .= $this->write_files($answer->answerfiles);
1508 $output .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1509 $output .= $this->writetext($answer->feedback, 4);
1510 $output .= $this->write_files($answer->feedbackfiles);
1511 $output .= " </feedback>\n";
1513 $output .= " </answer>\n";
1518 * Write out the hints.
1519 * @param object $question the question definition data.
1520 * @return string XML to output.
1522 public function write_hints($question) {
1523 if (empty($question->hints)) {
1528 foreach ($question->hints as $hint) {
1529 $output .= $this->write_hint($hint, $question->contextid);
1535 * @param int $format a FORMAT_... constant.
1536 * @return string the attribute to add to an XML tag.
1538 public function format($format) {
1539 return 'format="' . $this->get_format($format) . '"';
1542 public function write_hint($hint, $contextid) {
1543 $fs = get_file_storage();
1544 $files = $fs->get_area_files($contextid, 'question', 'hint', $hint->id);
1547 $output .= " <hint {$this->format($hint->hintformat)}>\n";
1548 $output .= ' ' . $this->writetext($hint->hint);
1550 if (!empty($hint->shownumcorrect)) {
1551 $output .= " <shownumcorrect/>\n";
1553 if (!empty($hint->clearwrong)) {
1554 $output .= " <clearwrong/>\n";
1557 if (!empty($hint->options)) {
1558 $output .= ' <options>' . $this->xml_escape($hint->options) . "</options>\n";
1560 $output .= $this->write_files($files);
1561 $output .= " </hint>\n";
1566 * Output the combined feedback fields.
1567 * @param object $questionoptions the question definition data.
1568 * @param int $questionid the question id.
1569 * @param int $contextid the question context id.
1570 * @return string XML to output.
1572 public function write_combined_feedback($questionoptions, $questionid, $contextid) {
1573 $fs = get_file_storage();
1576 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
1577 foreach ($fields as $field) {
1578 $formatfield = $field . 'format';
1579 $files = $fs->get_area_files($contextid, 'question', $field, $questionid);
1581 $output .= " <{$field} {$this->format($questionoptions->$formatfield)}>\n";
1582 $output .= ' ' . $this->writetext($questionoptions->$field);
1583 $output .= $this->write_files($files);
1584 $output .= " </{$field}>\n";
1587 if (!empty($questionoptions->shownumcorrect)) {
1588 $output .= " <shownumcorrect/>\n";