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 if (!empty($CFG->usetags
) && array_key_exists('tags', $question['#'])
259 && !empty($question['#']['tags'][0]['#']['tag'])) {
260 require_once($CFG->dirroot
.'/tag/lib.php');
262 foreach ($question['#']['tags'][0]['#']['tag'] as $tagdata) {
263 $qo->tags
[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true);
271 * Import the common parts of a single answer
272 * @param array answer xml tree for single answer
273 * @param bool $withanswerfiles if true, the answers are HTML (or $defaultformat)
274 * and so may contain files, otherwise the answers are plain text.
275 * @param array Default text format for the feedback, and the answers if $withanswerfiles
277 * @return object answer object
279 public function import_answer($answer, $withanswerfiles = false, $defaultformat = 'html') {
280 $ans = new stdClass();
282 if ($withanswerfiles) {
283 $ans->answer
= $this->import_text_with_files($answer, array(), '', $defaultformat);
285 $ans->answer
= array();
286 $ans->answer
['text'] = $this->getpath($answer, array('#', 'text', 0, '#'), '', true);
287 $ans->answer
['format'] = FORMAT_PLAIN
;
290 $ans->feedback
= $this->import_text_with_files($answer, array('#', 'feedback', 0), '', $defaultformat);
292 $ans->fraction
= $this->getpath($answer, array('@', 'fraction'), 0) / 100;
298 * Import the common overall feedback fields.
299 * @param object $question the part of the XML relating to this question.
300 * @param object $qo the question data to add the fields to.
301 * @param bool $withshownumpartscorrect include the shownumcorrect field.
303 public function import_combined_feedback($qo, $questionxml, $withshownumpartscorrect = false) {
304 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
305 foreach ($fields as $field) {
306 $qo->$field = $this->import_text_with_files($questionxml,
307 array('#', $field, 0), '', $this->get_format($qo->questiontextformat
));
310 if ($withshownumpartscorrect) {
311 $qo->shownumcorrect
= array_key_exists('shownumcorrect', $questionxml['#']);
313 // Backwards compatibility.
314 if (array_key_exists('correctresponsesfeedback', $questionxml['#'])) {
315 $qo->shownumcorrect
= $this->trans_single($this->getpath($questionxml,
316 array('#', 'correctresponsesfeedback', 0, '#'), 1));
322 * Import a question hint
323 * @param array $hintxml hint xml fragment.
324 * @param string $defaultformat the text format to assume for hints that do not specify.
325 * @return object hint for storing in the database.
327 public function import_hint($hintxml, $defaultformat) {
328 $hint = new stdClass();
329 if (array_key_exists('hintcontent', $hintxml['#'])) {
330 // Backwards compatibility.
332 $hint->hint
= $this->import_text_with_files($hintxml,
333 array('#', 'hintcontent', 0), '', $defaultformat);
335 $hint->shownumcorrect
= $this->getpath($hintxml,
336 array('#', 'statenumberofcorrectresponses', 0, '#'), 0);
337 $hint->clearwrong
= $this->getpath($hintxml,
338 array('#', 'clearincorrectresponses', 0, '#'), 0);
339 $hint->options
= $this->getpath($hintxml,
340 array('#', 'showfeedbacktoresponses', 0, '#'), 0);
344 $hint->hint
= $this->import_text_with_files($hintxml, array(), '', $defaultformat);
345 $hint->shownumcorrect
= array_key_exists('shownumcorrect', $hintxml['#']);
346 $hint->clearwrong
= array_key_exists('clearwrong', $hintxml['#']);
347 $hint->options
= $this->getpath($hintxml, array('#', 'options', 0, '#'), '', true);
353 * Import all the question hints
355 * @param object $qo the question data that is being constructed.
356 * @param array $questionxml The xml representing the question.
357 * @param bool $withparts whether the extra fields relating to parts should be imported.
358 * @param bool $withoptions whether the extra options field should be imported.
359 * @param string $defaultformat the text format to assume for hints that do not specify.
360 * @return array of objects representing the hints in the file.
362 public function import_hints($qo, $questionxml, $withparts = false,
363 $withoptions = false, $defaultformat = 'html') {
364 if (!isset($questionxml['#']['hint'])) {
368 foreach ($questionxml['#']['hint'] as $hintxml) {
369 $hint = $this->import_hint($hintxml, $defaultformat);
370 $qo->hint
[] = $hint->hint
;
373 $qo->hintshownumcorrect
[] = $hint->shownumcorrect
;
374 $qo->hintclearwrong
[] = $hint->clearwrong
;
378 $qo->hintoptions
[] = $hint->options
;
384 * Import files from a node in the XML.
385 * @param array $xml an array of <file> nodes from the the parsed XML.
386 * @return array of things representing files - in the form that save_question expects.
388 public function import_files($xml) {
390 foreach ($xml as $file) {
391 $data = new stdClass();
392 $data->content
= $file['#'];
393 $data->encoding
= $file['@']['encoding'];
394 $data->name
= $file['@']['name'];
401 * import multiple choice question
402 * @param array question question array from xml tree
403 * @return object question object
405 public function import_multichoice($question) {
407 $qo = $this->import_headers($question);
409 // Header parts particular to multichoice.
410 $qo->qtype
= 'multichoice';
411 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
412 $qo->single
= $this->trans_single($single);
413 $shuffleanswers = $this->getpath($question,
414 array('#', 'shuffleanswers', 0, '#'), 'false');
415 $qo->answernumbering
= $this->getpath($question,
416 array('#', 'answernumbering', 0, '#'), 'abc');
417 $qo->shuffleanswers
= $this->trans_single($shuffleanswers);
419 // There was a time on the 1.8 branch when it could output an empty
420 // answernumbering tag, so fix up any found.
421 if (empty($qo->answernumbering
)) {
422 $qo->answernumbering
= 'abc';
425 // Run through the answers.
426 $answers = $question['#']['answer'];
428 foreach ($answers as $answer) {
429 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat
));
430 $qo->answer
[$acount] = $ans->answer
;
431 $qo->fraction
[$acount] = $ans->fraction
;
432 $qo->feedback
[$acount] = $ans->feedback
;
436 $this->import_combined_feedback($qo, $question, true);
437 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat
));
443 * Import cloze type question
444 * @param array question question array from xml tree
445 * @return object question object
447 public function import_multianswer($question) {
449 question_bank
::get_qtype('multianswer');
451 $questiontext = $this->import_text_with_files($question,
452 array('#', 'questiontext', 0));
453 $qo = qtype_multianswer_extract_question($questiontext);
455 // Header parts particular to multianswer.
456 $qo->qtype
= 'multianswer';
457 $qo->course
= $this->course
;
459 $qo->name
= $this->clean_question_name($this->import_text($question['#']['name'][0]['#']['text']));
460 $qo->questiontextformat
= $questiontext['format'];
461 $qo->questiontext
= $qo->questiontext
['text'];
462 if (!empty($questiontext['itemid'])) {
463 $qo->questiontextitemid
= $questiontext['itemid'];
466 // Backwards compatibility, deal with the old image tag.
467 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
468 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
469 if ($filedata && $filename) {
470 $fs = get_file_storage();
471 if (empty($qo->questiontextitemid
)) {
472 $qo->questiontextitemid
= file_get_unused_draft_itemid();
474 $filename = clean_param(str_replace('/', '_', $filename), PARAM_FILE
);
476 'contextid' => context_user
::instance($USER->id
)->id
,
477 'component' => 'user',
478 'filearea' => 'draft',
479 'itemid' => $qo->questiontextitemid
,
481 'filename' => $filename,
483 $fs->create_file_from_string($filerecord, base64_decode($filedata));
484 $qo->questiontext
.= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
487 // Restore files in generalfeedback.
488 $generalfeedback = $this->import_text_with_files($question,
489 array('#', 'generalfeedback', 0), $qo->generalfeedback
, $this->get_format($qo->questiontextformat
));
490 $qo->generalfeedback
= $generalfeedback['text'];
491 $qo->generalfeedbackformat
= $generalfeedback['format'];
492 if (!empty($generalfeedback['itemid'])) {
493 $qo->generalfeedbackitemid
= $generalfeedback['itemid'];
496 $qo->penalty
= $this->getpath($question,
497 array('#', 'penalty', 0, '#'), $this->defaultquestion()->penalty
);
498 // Fix problematic rounding from old files.
499 if (abs($qo->penalty
- 0.3333333) < 0.005) {
500 $qo->penalty
= 0.3333333;
503 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat
));
509 * Import true/false type question
510 * @param array question question array from xml tree
511 * @return object question object
513 public function import_truefalse($question) {
516 $qo = $this->import_headers($question);
518 // Header parts particular to true/false.
519 $qo->qtype
= 'truefalse';
521 // In the past, it used to be assumed that the two answers were in the file
522 // true first, then false. Howevever that was not always true. Now, we
523 // try to match on the answer text, but in old exports, this will be a localised
524 // string, so if we don't find true or false, we fall back to the old system.
527 foreach ($question['#']['answer'] as $answer) {
528 $answertext = $this->getpath($answer,
529 array('#', 'text', 0, '#'), '', true);
530 $feedback = $this->import_text_with_files($answer,
531 array('#', 'feedback', 0), '', $this->get_format($qo->questiontextformat
));
533 if ($answertext != 'true' && $answertext != 'false') {
534 // Old style file, assume order is true/false.
537 $answertext = 'true';
539 $answertext = 'false';
543 if ($answertext == 'true') {
544 $qo->answer
= ($answer['@']['fraction'] == 100);
545 $qo->correctanswer
= $qo->answer
;
546 $qo->feedbacktrue
= $feedback;
548 $qo->answer
= ($answer['@']['fraction'] != 100);
549 $qo->correctanswer
= $qo->answer
;
550 $qo->feedbackfalse
= $feedback;
557 $a->questiontext
= $qo->questiontext
;
558 $a->answer
= get_string($qo->correctanswer ?
'true' : 'false', 'qtype_truefalse');
559 echo $OUTPUT->notification(get_string('truefalseimporterror', 'qformat_xml', $a));
562 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat
));
568 * Import short answer type question
569 * @param array question question array from xml tree
570 * @return object question object
572 public function import_shortanswer($question) {
574 $qo = $this->import_headers($question);
576 // Header parts particular to shortanswer.
577 $qo->qtype
= 'shortanswer';
580 $qo->usecase
= $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase
);
582 // Run through the answers.
583 $answers = $question['#']['answer'];
585 foreach ($answers as $answer) {
586 $ans = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat
));
587 $qo->answer
[$acount] = $ans->answer
['text'];
588 $qo->fraction
[$acount] = $ans->fraction
;
589 $qo->feedback
[$acount] = $ans->feedback
;
593 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat
));
599 * Import description type question
600 * @param array question question array from xml tree
601 * @return object question object
603 public function import_description($question) {
605 $qo = $this->import_headers($question);
606 // Header parts particular to shortanswer.
607 $qo->qtype
= 'description';
608 $qo->defaultmark
= 0;
614 * Import numerical type question
615 * @param array question question array from xml tree
616 * @return object question object
618 public function import_numerical($question) {
620 $qo = $this->import_headers($question);
622 // Header parts particular to numerical.
623 $qo->qtype
= 'numerical';
625 // Get answers array.
626 $answers = $question['#']['answer'];
627 $qo->answer
= array();
628 $qo->feedback
= array();
629 $qo->fraction
= array();
630 $qo->tolerance
= array();
631 foreach ($answers as $answer) {
632 // Answer outside of <text> is deprecated.
633 $obj = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat
));
634 $qo->answer
[] = $obj->answer
['text'];
635 if (empty($qo->answer
)) {
638 $qo->feedback
[] = $obj->feedback
;
639 $qo->tolerance
[] = $this->getpath($answer, array('#', 'tolerance', 0, '#'), 0);
641 // Fraction as a tag is deprecated.
642 $fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
643 $qo->fraction
[] = $this->getpath($answer,
644 array('#', 'fraction', 0, '#'), $fraction); // Deprecated.
647 // Get the units array.
649 $units = $this->getpath($question, array('#', 'units', 0, '#', 'unit'), array());
650 if (!empty($units)) {
651 $qo->multiplier
= array();
652 foreach ($units as $unit) {
653 $qo->multiplier
[] = $this->getpath($unit, array('#', 'multiplier', 0, '#'), 1);
654 $qo->unit
[] = $this->getpath($unit, array('#', 'unit_name', 0, '#'), '', true);
657 $qo->unitgradingtype
= $this->getpath($question, array('#', 'unitgradingtype', 0, '#'), 0);
658 $qo->unitpenalty
= $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0.1);
659 $qo->showunits
= $this->getpath($question, array('#', 'showunits', 0, '#'), null);
660 $qo->unitsleft
= $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
661 $qo->instructions
['text'] = '';
662 $qo->instructions
['format'] = FORMAT_HTML
;
663 $instructions = $this->getpath($question, array('#', 'instructions'), array());
664 if (!empty($instructions)) {
665 $qo->instructions
= $this->import_text_with_files($instructions,
666 array('0'), '', $this->get_format($qo->questiontextformat
));
669 if (is_null($qo->showunits
)) {
670 // Set a good default, depending on whether there are any units defined.
671 if (empty($qo->unit
)) {
672 $qo->showunits
= 3; // This is qtype_numerical::UNITNONE, but we cannot refer to that constant here.
674 $qo->showunits
= 0; // This is qtype_numerical::UNITOPTIONAL, but we cannot refer to that constant here.
678 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat
));
684 * Import matching type question
685 * @param array question question array from xml tree
686 * @return object question object
688 public function import_match($question) {
690 $qo = $this->import_headers($question);
692 // Header parts particular to matching.
693 $qo->qtype
= 'match';
694 $qo->shuffleanswers
= $this->trans_single($this->getpath($question,
695 array('#', 'shuffleanswers', 0, '#'), 1));
697 // Run through subquestions.
698 $qo->subquestions
= array();
699 $qo->subanswers
= array();
700 foreach ($question['#']['subquestion'] as $subqxml) {
701 $qo->subquestions
[] = $this->import_text_with_files($subqxml,
702 array(), '', $this->get_format($qo->questiontextformat
));
704 $answers = $this->getpath($subqxml, array('#', 'answer'), array());
705 $qo->subanswers
[] = $this->getpath($subqxml,
706 array('#', 'answer', 0, '#', 'text', 0, '#'), '', true);
709 $this->import_combined_feedback($qo, $question, true);
710 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat
));
716 * Import essay type question
717 * @param array question question array from xml tree
718 * @return object question object
720 public function import_essay($question) {
722 $qo = $this->import_headers($question);
724 // Header parts particular to essay.
725 $qo->qtype
= 'essay';
727 $qo->responseformat
= $this->getpath($question,
728 array('#', 'responseformat', 0, '#'), 'editor');
729 $qo->responsefieldlines
= $this->getpath($question,
730 array('#', 'responsefieldlines', 0, '#'), 15);
731 $qo->responserequired
= $this->getpath($question,
732 array('#', 'responserequired', 0, '#'), 1);
733 $qo->attachments
= $this->getpath($question,
734 array('#', 'attachments', 0, '#'), 0);
735 $qo->attachmentsrequired
= $this->getpath($question,
736 array('#', 'attachmentsrequired', 0, '#'), 0);
737 $qo->graderinfo
= $this->import_text_with_files($question,
738 array('#', 'graderinfo', 0), '', $this->get_format($qo->questiontextformat
));
739 $qo->responsetemplate
['text'] = $this->getpath($question,
740 array('#', 'responsetemplate', 0, '#', 'text', 0, '#'), '', true);
741 $qo->responsetemplate
['format'] = $this->trans_format($this->getpath($question,
742 array('#', 'responsetemplate', 0, '@', 'format'), $this->get_format($qo->questiontextformat
)));
748 * Import a calculated question
749 * @param object $question the imported XML data.
751 public function import_calculated($question) {
754 $qo = $this->import_headers($question);
756 // Header parts particular to calculated.
757 $qo->qtype
= 'calculated';
758 $qo->synchronize
= $this->getpath($question, array('#', 'synchronize', 0, '#'), 0);
759 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
760 $qo->single
= $this->trans_single($single);
761 $shuffleanswers = $this->getpath($question, array('#', 'shuffleanswers', 0, '#'), 'false');
762 $qo->answernumbering
= $this->getpath($question,
763 array('#', 'answernumbering', 0, '#'), 'abc');
764 $qo->shuffleanswers
= $this->trans_single($shuffleanswers);
766 $this->import_combined_feedback($qo, $question);
768 $qo->unitgradingtype
= $this->getpath($question,
769 array('#', 'unitgradingtype', 0, '#'), 0);
770 $qo->unitpenalty
= $this->getpath($question, array('#', 'unitpenalty', 0, '#'), null);
771 $qo->showunits
= $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
772 $qo->unitsleft
= $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
773 $qo->instructions
= $this->getpath($question,
774 array('#', 'instructions', 0, '#', 'text', 0, '#'), '', true);
775 if (!empty($instructions)) {
776 $qo->instructions
= $this->import_text_with_files($instructions,
777 array('0'), '', $this->get_format($qo->questiontextformat
));
780 // Get answers array.
781 $answers = $question['#']['answer'];
782 $qo->answer
= array();
783 $qo->feedback
= array();
784 $qo->fraction
= array();
785 $qo->tolerance
= array();
786 $qo->tolerancetype
= array();
787 $qo->correctanswerformat
= array();
788 $qo->correctanswerlength
= array();
789 $qo->feedback
= array();
790 foreach ($answers as $answer) {
791 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat
));
792 // Answer outside of <text> is deprecated.
793 if (empty($ans->answer
['text'])) {
794 $ans->answer
['text'] = '*';
796 $qo->answer
[] = $ans->answer
['text'];
797 $qo->feedback
[] = $ans->feedback
;
798 $qo->tolerance
[] = $answer['#']['tolerance'][0]['#'];
799 // Fraction as a tag is deprecated.
800 if (!empty($answer['#']['fraction'][0]['#'])) {
801 $qo->fraction
[] = $answer['#']['fraction'][0]['#'];
803 $qo->fraction
[] = $answer['@']['fraction'] / 100;
805 $qo->tolerancetype
[] = $answer['#']['tolerancetype'][0]['#'];
806 $qo->correctanswerformat
[] = $answer['#']['correctanswerformat'][0]['#'];
807 $qo->correctanswerlength
[] = $answer['#']['correctanswerlength'][0]['#'];
811 if (isset($question['#']['units'][0]['#']['unit'])) {
812 $units = $question['#']['units'][0]['#']['unit'];
813 $qo->multiplier
= array();
814 foreach ($units as $unit) {
815 $qo->multiplier
[] = $unit['#']['multiplier'][0]['#'];
816 $qo->unit
[] = $unit['#']['unit_name'][0]['#'];
819 $instructions = $this->getpath($question, array('#', 'instructions'), array());
820 if (!empty($instructions)) {
821 $qo->instructions
= $this->import_text_with_files($instructions,
822 array('0'), '', $this->get_format($qo->questiontextformat
));
825 if (is_null($qo->unitpenalty
)) {
826 // Set a good default, depending on whether there are any units defined.
827 if (empty($qo->unit
)) {
828 $qo->showunits
= 3; // This is qtype_numerical::UNITNONE, but we cannot refer to that constant here.
830 $qo->showunits
= 0; // This is qtype_numerical::UNITOPTIONAL, but we cannot refer to that constant here.
834 $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition'];
835 $qo->dataset
= array();
836 $qo->datasetindex
= 0;
837 foreach ($datasets as $dataset) {
839 $qo->dataset
[$qo->datasetindex
] = new stdClass();
840 $qo->dataset
[$qo->datasetindex
]->status
=
841 $this->import_text($dataset['#']['status'][0]['#']['text']);
842 $qo->dataset
[$qo->datasetindex
]->name
=
843 $this->import_text($dataset['#']['name'][0]['#']['text']);
844 $qo->dataset
[$qo->datasetindex
]->type
=
845 $dataset['#']['type'][0]['#'];
846 $qo->dataset
[$qo->datasetindex
]->distribution
=
847 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
848 $qo->dataset
[$qo->datasetindex
]->max
=
849 $this->import_text($dataset['#']['maximum'][0]['#']['text']);
850 $qo->dataset
[$qo->datasetindex
]->min
=
851 $this->import_text($dataset['#']['minimum'][0]['#']['text']);
852 $qo->dataset
[$qo->datasetindex
]->length
=
853 $this->import_text($dataset['#']['decimals'][0]['#']['text']);
854 $qo->dataset
[$qo->datasetindex
]->distribution
=
855 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
856 $qo->dataset
[$qo->datasetindex
]->itemcount
= $dataset['#']['itemcount'][0]['#'];
857 $qo->dataset
[$qo->datasetindex
]->datasetitem
= array();
858 $qo->dataset
[$qo->datasetindex
]->itemindex
= 0;
859 $qo->dataset
[$qo->datasetindex
]->number_of_items
= $this->getpath($dataset,
860 array('#', 'number_of_items', 0, '#'), 0);
861 $datasetitems = $this->getpath($dataset,
862 array('#', 'dataset_items', 0, '#', 'dataset_item'), array());
863 foreach ($datasetitems as $datasetitem) {
864 $qo->dataset
[$qo->datasetindex
]->itemindex++
;
865 $qo->dataset
[$qo->datasetindex
]->datasetitem
[
866 $qo->dataset
[$qo->datasetindex
]->itemindex
] = new stdClass();
867 $qo->dataset
[$qo->datasetindex
]->datasetitem
[
868 $qo->dataset
[$qo->datasetindex
]->itemindex
]->itemnumber
=
869 $datasetitem['#']['number'][0]['#'];
870 $qo->dataset
[$qo->datasetindex
]->datasetitem
[
871 $qo->dataset
[$qo->datasetindex
]->itemindex
]->value
=
872 $datasetitem['#']['value'][0]['#'];
876 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat
));
882 * This is not a real question type. It's a dummy type used to specify the
883 * import category. The format is:
884 * <question type="category">
885 * <category>tom/dick/harry</category>
888 protected function import_category($question) {
889 $qo = new stdClass();
890 $qo->qtype
= 'category';
891 $qo->category
= $this->import_text($question['#']['category'][0]['#']['text']);
896 * Parse the array of lines into an array of questions
897 * this *could* burn memory - but it won't happen that much
898 * so fingers crossed!
899 * @param array of lines from the input file.
900 * @param stdClass $context
901 * @return array (of objects) question objects.
903 protected function readquestions($lines) {
904 // We just need it as one big string.
905 $lines = implode('', $lines);
907 // This converts xml to big nasty data structure
908 // the 0 means keep white space as it is (important for markdown format).
910 $xml = xmlize($lines, 0, 'UTF-8', true);
911 } catch (xml_format_exception
$e) {
912 $this->error($e->getMessage(), '');
915 unset($lines); // No need to keep this in memory.
916 return $this->import_questions($xml['quiz']['#']['question']);
920 * @param array $xml the xmlized xml
921 * @return stdClass[] question objects to pass to question type save_question_options
923 public function import_questions($xml) {
924 $questions = array();
926 // Iterate through questions.
927 foreach ($xml as $questionxml) {
928 $qo = $this->import_question($questionxml);
930 // Stick the result in the $questions array.
939 * @param array $questionxml xml describing the question
940 * @return null|stdClass an object with data to be fed to question type save_question_options
942 protected function import_question($questionxml) {
943 $questiontype = $questionxml['@']['type'];
945 if ($questiontype == 'multichoice') {
946 return $this->import_multichoice($questionxml);
947 } else if ($questiontype == 'truefalse') {
948 return $this->import_truefalse($questionxml);
949 } else if ($questiontype == 'shortanswer') {
950 return $this->import_shortanswer($questionxml);
951 } else if ($questiontype == 'numerical') {
952 return $this->import_numerical($questionxml);
953 } else if ($questiontype == 'description') {
954 return $this->import_description($questionxml);
955 } else if ($questiontype == 'matching' ||
$questiontype == 'match') {
956 return $this->import_match($questionxml);
957 } else if ($questiontype == 'cloze' ||
$questiontype == 'multianswer') {
958 return $this->import_multianswer($questionxml);
959 } else if ($questiontype == 'essay') {
960 return $this->import_essay($questionxml);
961 } else if ($questiontype == 'calculated') {
962 return $this->import_calculated($questionxml);
963 } else if ($questiontype == 'calculatedsimple') {
964 $qo = $this->import_calculated($questionxml);
965 $qo->qtype
= 'calculatedsimple';
967 } else if ($questiontype == 'calculatedmulti') {
968 $qo = $this->import_calculated($questionxml);
969 $qo->qtype
= 'calculatedmulti';
971 } else if ($questiontype == 'category') {
972 return $this->import_category($questionxml);
975 // Not a type we handle ourselves. See if the question type wants
977 if (!$qo = $this->try_importing_using_qtypes($questionxml, null, null, $questiontype)) {
978 $this->error(get_string('xmltypeunsupported', 'qformat_xml', $questiontype));
985 // EXPORT FUNCTIONS START HERE.
987 public function export_file_extension() {
992 * Turn the internal question type name into a human readable form.
993 * (In the past, the code used to use integers internally. Now, it uses
994 * strings, so there is less need for this, but to maintain
995 * backwards-compatibility we change two of the type names.)
996 * @param string $qtype question type plugin name.
997 * @return string $qtype string to use in the file.
999 protected function get_qtype($qtype) {
1011 * Convert internal Moodle text format code into
1012 * human readable form
1013 * @param int id internal code
1014 * @return string format text
1016 public function get_format($id) {
1019 return 'moodle_auto_format';
1023 return 'plain_text';
1026 case FORMAT_MARKDOWN
:
1034 * Convert internal single question code into
1035 * human readable form
1036 * @param int id single question code
1037 * @return string single question string
1039 public function get_single($id) {
1051 * Take a string, and wrap it in a CDATA secion, if that is required to make
1052 * the output XML valid.
1053 * @param string $string a string
1054 * @return string the string, wrapped in CDATA if necessary.
1056 public function xml_escape($string) {
1057 if (!empty($string) && htmlspecialchars($string) != $string) {
1058 return "<![CDATA[{$string}]]>";
1065 * Generates <text></text> tags, processing raw text therein
1066 * @param string $raw the content to output.
1067 * @param int $indent the current indent level.
1068 * @param bool $short stick it on one line.
1069 * @return string formatted text.
1071 public function writetext($raw, $indent = 0, $short = true) {
1072 $indent = str_repeat(' ', $indent);
1073 $raw = $this->xml_escape($raw);
1076 $xml = "{$indent}<text>{$raw}</text>\n";
1078 $xml = "{$indent}<text>\n{$raw}\n{$indent}</text>\n";
1085 * Generte the XML to represent some files.
1086 * @param array of store array of stored_file objects.
1087 * @return string $string the XML.
1089 public function write_files($files) {
1090 if (empty($files)) {
1094 foreach ($files as $file) {
1095 if ($file->is_directory()) {
1098 $string .= '<file name="' . $file->get_filename() . '" path="' . $file->get_filepath() . '" encoding="base64">';
1099 $string .= base64_encode($file->get_content());
1100 $string .= "</file>\n";
1105 protected function presave_process($content) {
1106 // Override to allow us to add xml headers and footers.
1107 return '<?xml version="1.0" encoding="UTF-8"?>
1109 ' . $content . '</quiz>';
1113 * Turns question into an xml segment
1114 * @param object $question the question data.
1115 * @return string xml segment
1117 public function writequestion($question) {
1118 global $CFG, $OUTPUT;
1120 $invalidquestion = false;
1121 $fs = get_file_storage();
1122 $contextid = $question->contextid
;
1123 // Get files used by the questiontext.
1124 $question->questiontextfiles
= $fs->get_area_files(
1125 $contextid, 'question', 'questiontext', $question->id
);
1126 // Get files used by the generalfeedback.
1127 $question->generalfeedbackfiles
= $fs->get_area_files(
1128 $contextid, 'question', 'generalfeedback', $question->id
);
1129 if (!empty($question->options
->answers
)) {
1130 foreach ($question->options
->answers
as $answer) {
1131 $answer->answerfiles
= $fs->get_area_files(
1132 $contextid, 'question', 'answer', $answer->id
);
1133 $answer->feedbackfiles
= $fs->get_area_files(
1134 $contextid, 'question', 'answerfeedback', $answer->id
);
1140 // Add a comment linking this to the original question id.
1141 $expout .= "<!-- question: {$question->id} -->\n";
1143 // Check question type.
1144 $questiontype = $this->get_qtype($question->qtype
);
1146 // Categories are a special case.
1147 if ($question->qtype
== 'category') {
1148 $categorypath = $this->writetext($question->category
);
1149 $expout .= " <question type=\"category\">\n";
1150 $expout .= " <category>\n";
1151 $expout .= " {$categorypath}\n";
1152 $expout .= " </category>\n";
1153 $expout .= " </question>\n";
1157 // Now we know we are are handing a real question.
1158 // Output the generic information.
1159 $expout .= " <question type=\"{$questiontype}\">\n";
1160 $expout .= " <name>\n";
1161 $expout .= $this->writetext($question->name
, 3);
1162 $expout .= " </name>\n";
1163 $expout .= " <questiontext {$this->format($question->questiontextformat)}>\n";
1164 $expout .= $this->writetext($question->questiontext
, 3);
1165 $expout .= $this->write_files($question->questiontextfiles
);
1166 $expout .= " </questiontext>\n";
1167 $expout .= " <generalfeedback {$this->format($question->generalfeedbackformat)}>\n";
1168 $expout .= $this->writetext($question->generalfeedback
, 3);
1169 $expout .= $this->write_files($question->generalfeedbackfiles
);
1170 $expout .= " </generalfeedback>\n";
1171 if ($question->qtype
!= 'multianswer') {
1172 $expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
1174 $expout .= " <penalty>{$question->penalty}</penalty>\n";
1175 $expout .= " <hidden>{$question->hidden}</hidden>\n";
1177 // The rest of the output depends on question type.
1178 switch($question->qtype
) {
1180 // Not a qtype really - dummy used for category switching.
1184 $trueanswer = $question->options
->answers
[$question->options
->trueanswer
];
1185 $trueanswer->answer
= 'true';
1186 $expout .= $this->write_answer($trueanswer);
1188 $falseanswer = $question->options
->answers
[$question->options
->falseanswer
];
1189 $falseanswer->answer
= 'false';
1190 $expout .= $this->write_answer($falseanswer);
1194 $expout .= " <single>" . $this->get_single($question->options
->single
) .
1196 $expout .= " <shuffleanswers>" .
1197 $this->get_single($question->options
->shuffleanswers
) .
1198 "</shuffleanswers>\n";
1199 $expout .= " <answernumbering>" . $question->options
->answernumbering
.
1200 "</answernumbering>\n";
1201 $expout .= $this->write_combined_feedback($question->options
, $question->id
, $question->contextid
);
1202 $expout .= $this->write_answers($question->options
->answers
);
1206 $expout .= " <usecase>{$question->options->usecase}</usecase>\n";
1207 $expout .= $this->write_answers($question->options
->answers
);
1211 foreach ($question->options
->answers
as $answer) {
1212 $expout .= $this->write_answer($answer,
1213 " <tolerance>{$answer->tolerance}</tolerance>\n");
1216 $units = $question->options
->units
;
1217 if (count($units)) {
1218 $expout .= "<units>\n";
1219 foreach ($units as $unit) {
1220 $expout .= " <unit>\n";
1221 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1222 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1223 $expout .= " </unit>\n";
1225 $expout .= "</units>\n";
1227 if (isset($question->options
->unitgradingtype
)) {
1228 $expout .= " <unitgradingtype>" . $question->options
->unitgradingtype
.
1229 "</unitgradingtype>\n";
1231 if (isset($question->options
->unitpenalty
)) {
1232 $expout .= " <unitpenalty>{$question->options->unitpenalty}</unitpenalty>\n";
1234 if (isset($question->options
->showunits
)) {
1235 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1237 if (isset($question->options
->unitsleft
)) {
1238 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1240 if (!empty($question->options
->instructionsformat
)) {
1241 $files = $fs->get_area_files($contextid, 'qtype_numerical',
1242 'instruction', $question->id
);
1243 $expout .= " <instructions " .
1244 $this->format($question->options
->instructionsformat
) . ">\n";
1245 $expout .= $this->writetext($question->options
->instructions
, 3);
1246 $expout .= $this->write_files($files);
1247 $expout .= " </instructions>\n";
1252 $expout .= " <shuffleanswers>" .
1253 $this->get_single($question->options
->shuffleanswers
) .
1254 "</shuffleanswers>\n";
1255 $expout .= $this->write_combined_feedback($question->options
, $question->id
, $question->contextid
);
1256 foreach ($question->options
->subquestions
as $subquestion) {
1257 $files = $fs->get_area_files($contextid, 'qtype_match',
1258 'subquestion', $subquestion->id
);
1259 $expout .= " <subquestion " .
1260 $this->format($subquestion->questiontextformat
) . ">\n";
1261 $expout .= $this->writetext($subquestion->questiontext
, 3);
1262 $expout .= $this->write_files($files);
1263 $expout .= " <answer>\n";
1264 $expout .= $this->writetext($subquestion->answertext
, 4);
1265 $expout .= " </answer>\n";
1266 $expout .= " </subquestion>\n";
1271 // Nothing else to do.
1275 foreach ($question->options
->questions
as $index => $subq) {
1276 $expout = str_replace('{#' . $index . '}', $subq->questiontext
, $expout);
1281 $expout .= " <responseformat>" . $question->options
->responseformat
.
1282 "</responseformat>\n";
1283 $expout .= " <responserequired>" . $question->options
->responserequired
.
1284 "</responserequired>\n";
1285 $expout .= " <responsefieldlines>" . $question->options
->responsefieldlines
.
1286 "</responsefieldlines>\n";
1287 $expout .= " <attachments>" . $question->options
->attachments
.
1289 $expout .= " <attachmentsrequired>" . $question->options
->attachmentsrequired
.
1290 "</attachmentsrequired>\n";
1291 $expout .= " <graderinfo " .
1292 $this->format($question->options
->graderinfoformat
) . ">\n";
1293 $expout .= $this->writetext($question->options
->graderinfo
, 3);
1294 $expout .= $this->write_files($fs->get_area_files($contextid, 'qtype_essay',
1295 'graderinfo', $question->id
));
1296 $expout .= " </graderinfo>\n";
1297 $expout .= " <responsetemplate " .
1298 $this->format($question->options
->responsetemplateformat
) . ">\n";
1299 $expout .= $this->writetext($question->options
->responsetemplate
, 3);
1300 $expout .= " </responsetemplate>\n";
1304 case 'calculatedsimple':
1305 case 'calculatedmulti':
1306 $expout .= " <synchronize>{$question->options->synchronize}</synchronize>\n";
1307 $expout .= " <single>{$question->options->single}</single>\n";
1308 $expout .= " <answernumbering>" . $question->options
->answernumbering
.
1309 "</answernumbering>\n";
1310 $expout .= " <shuffleanswers>" . $question->options
->shuffleanswers
.
1311 "</shuffleanswers>\n";
1313 $component = 'qtype_' . $question->qtype
;
1314 $files = $fs->get_area_files($contextid, $component,
1315 'correctfeedback', $question->id
);
1316 $expout .= " <correctfeedback>\n";
1317 $expout .= $this->writetext($question->options
->correctfeedback
, 3);
1318 $expout .= $this->write_files($files);
1319 $expout .= " </correctfeedback>\n";
1321 $files = $fs->get_area_files($contextid, $component,
1322 'partiallycorrectfeedback', $question->id
);
1323 $expout .= " <partiallycorrectfeedback>\n";
1324 $expout .= $this->writetext($question->options
->partiallycorrectfeedback
, 3);
1325 $expout .= $this->write_files($files);
1326 $expout .= " </partiallycorrectfeedback>\n";
1328 $files = $fs->get_area_files($contextid, $component,
1329 'incorrectfeedback', $question->id
);
1330 $expout .= " <incorrectfeedback>\n";
1331 $expout .= $this->writetext($question->options
->incorrectfeedback
, 3);
1332 $expout .= $this->write_files($files);
1333 $expout .= " </incorrectfeedback>\n";
1335 foreach ($question->options
->answers
as $answer) {
1336 $percent = 100 * $answer->fraction
;
1337 $expout .= "<answer fraction=\"{$percent}\">\n";
1338 // The "<text/>" tags are an added feature, old files won't have them.
1339 $expout .= " <text>{$answer->answer}</text>\n";
1340 $expout .= " <tolerance>{$answer->tolerance}</tolerance>\n";
1341 $expout .= " <tolerancetype>{$answer->tolerancetype}</tolerancetype>\n";
1342 $expout .= " <correctanswerformat>" .
1343 $answer->correctanswerformat
. "</correctanswerformat>\n";
1344 $expout .= " <correctanswerlength>" .
1345 $answer->correctanswerlength
. "</correctanswerlength>\n";
1346 $expout .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1347 $files = $fs->get_area_files($contextid, $component,
1348 'instruction', $question->id
);
1349 $expout .= $this->writetext($answer->feedback
);
1350 $expout .= $this->write_files($answer->feedbackfiles
);
1351 $expout .= " </feedback>\n";
1352 $expout .= "</answer>\n";
1354 if (isset($question->options
->unitgradingtype
)) {
1355 $expout .= " <unitgradingtype>" .
1356 $question->options
->unitgradingtype
. "</unitgradingtype>\n";
1358 if (isset($question->options
->unitpenalty
)) {
1359 $expout .= " <unitpenalty>" .
1360 $question->options
->unitpenalty
. "</unitpenalty>\n";
1362 if (isset($question->options
->showunits
)) {
1363 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1365 if (isset($question->options
->unitsleft
)) {
1366 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1369 if (isset($question->options
->instructionsformat
)) {
1370 $files = $fs->get_area_files($contextid, $component,
1371 'instruction', $question->id
);
1372 $expout .= " <instructions " .
1373 $this->format($question->options
->instructionsformat
) . ">\n";
1374 $expout .= $this->writetext($question->options
->instructions
, 3);
1375 $expout .= $this->write_files($files);
1376 $expout .= " </instructions>\n";
1379 if (isset($question->options
->units
)) {
1380 $units = $question->options
->units
;
1381 if (count($units)) {
1382 $expout .= "<units>\n";
1383 foreach ($units as $unit) {
1384 $expout .= " <unit>\n";
1385 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1386 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1387 $expout .= " </unit>\n";
1389 $expout .= "</units>\n";
1393 // The tag $question->export_process has been set so we get all the
1394 // data items in the database from the function
1395 // qtype_calculated::get_question_options calculatedsimple defaults
1397 if (isset($question->options
->datasets
) && count($question->options
->datasets
)) {
1398 $expout .= "<dataset_definitions>\n";
1399 foreach ($question->options
->datasets
as $def) {
1400 $expout .= "<dataset_definition>\n";
1401 $expout .= " <status>".$this->writetext($def->status
)."</status>\n";
1402 $expout .= " <name>".$this->writetext($def->name
)."</name>\n";
1403 if ($question->qtype
== 'calculated') {
1404 $expout .= " <type>calculated</type>\n";
1406 $expout .= " <type>calculatedsimple</type>\n";
1408 $expout .= " <distribution>" . $this->writetext($def->distribution
) .
1409 "</distribution>\n";
1410 $expout .= " <minimum>" . $this->writetext($def->minimum
) .
1412 $expout .= " <maximum>" . $this->writetext($def->maximum
) .
1414 $expout .= " <decimals>" . $this->writetext($def->decimals
) .
1416 $expout .= " <itemcount>{$def->itemcount}</itemcount>\n";
1417 if ($def->itemcount
> 0) {
1418 $expout .= " <dataset_items>\n";
1419 foreach ($def->items
as $item) {
1420 $expout .= " <dataset_item>\n";
1421 $expout .= " <number>".$item->itemnumber
."</number>\n";
1422 $expout .= " <value>".$item->value
."</value>\n";
1423 $expout .= " </dataset_item>\n";
1425 $expout .= " </dataset_items>\n";
1426 $expout .= " <number_of_items>" . $def->number_of_items
.
1427 "</number_of_items>\n";
1429 $expout .= "</dataset_definition>\n";
1431 $expout .= "</dataset_definitions>\n";
1436 // Try support by optional plugin.
1437 if (!$data = $this->try_exporting_using_qtypes($question->qtype
, $question)) {
1438 $invalidquestion = true;
1444 // Output any hints.
1445 $expout .= $this->write_hints($question);
1447 // Write the question tags.
1448 if (!empty($CFG->usetags
)) {
1449 require_once($CFG->dirroot
.'/tag/lib.php');
1450 $tags = tag_get_tags_array('question', $question->id
);
1451 if (!empty($tags)) {
1452 $expout .= " <tags>\n";
1453 foreach ($tags as $tag) {
1454 $expout .= " <tag>" . $this->writetext($tag, 0, true) . "</tag>\n";
1456 $expout .= " </tags>\n";
1460 // Close the question tag.
1461 $expout .= " </question>\n";
1462 if ($invalidquestion) {
1469 public function write_answers($answers) {
1470 if (empty($answers)) {
1474 foreach ($answers as $answer) {
1475 $output .= $this->write_answer($answer);
1480 public function write_answer($answer, $extra = '') {
1481 $percent = $answer->fraction
* 100;
1483 $output .= " <answer fraction=\"{$percent}\" {$this->format($answer->answerformat)}>\n";
1484 $output .= $this->writetext($answer->answer
, 3);
1485 $output .= $this->write_files($answer->answerfiles
);
1486 $output .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1487 $output .= $this->writetext($answer->feedback
, 4);
1488 $output .= $this->write_files($answer->feedbackfiles
);
1489 $output .= " </feedback>\n";
1491 $output .= " </answer>\n";
1496 * Write out the hints.
1497 * @param object $question the question definition data.
1498 * @return string XML to output.
1500 public function write_hints($question) {
1501 if (empty($question->hints
)) {
1506 foreach ($question->hints
as $hint) {
1507 $output .= $this->write_hint($hint, $question->contextid
);
1513 * @param int $format a FORMAT_... constant.
1514 * @return string the attribute to add to an XML tag.
1516 public function format($format) {
1517 return 'format="' . $this->get_format($format) . '"';
1520 public function write_hint($hint, $contextid) {
1521 $fs = get_file_storage();
1522 $files = $fs->get_area_files($contextid, 'question', 'hint', $hint->id
);
1525 $output .= " <hint {$this->format($hint->hintformat)}>\n";
1526 $output .= ' ' . $this->writetext($hint->hint
);
1528 if (!empty($hint->shownumcorrect
)) {
1529 $output .= " <shownumcorrect/>\n";
1531 if (!empty($hint->clearwrong
)) {
1532 $output .= " <clearwrong/>\n";
1535 if (!empty($hint->options
)) {
1536 $output .= ' <options>' . $this->xml_escape($hint->options
) . "</options>\n";
1538 $output .= $this->write_files($files);
1539 $output .= " </hint>\n";
1544 * Output the combined feedback fields.
1545 * @param object $questionoptions the question definition data.
1546 * @param int $questionid the question id.
1547 * @param int $contextid the question context id.
1548 * @return string XML to output.
1550 public function write_combined_feedback($questionoptions, $questionid, $contextid) {
1551 $fs = get_file_storage();
1554 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
1555 foreach ($fields as $field) {
1556 $formatfield = $field . 'format';
1557 $files = $fs->get_area_files($contextid, 'question', $field, $questionid);
1559 $output .= " <{$field} {$this->format($questionoptions->$formatfield)}>\n";
1560 $output .= ' ' . $this->writetext($questionoptions->$field);
1561 $output .= $this->write_files($files);
1562 $output .= " </{$field}>\n";
1565 if (!empty($questionoptions->shownumcorrect
)) {
1566 $output .= " <shownumcorrect/>\n";