MDL-27797 fix all the codechecker issues in format_xml
[moodle.git] / question / format / xml / format.php
blob8a0a7b1dd3827b6d816988994a11fea95ca2f235
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
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.
8 //
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/>.
17 /**
18 * Code for exporting questions as Moodle XML.
20 * @package qformat
21 * @subpackage xml
22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/question/format.php');
30 require_once($CFG->libdir . '/xmlize.php');
33 /**
34 * Importer for Moodle XML question format.
36 * See http://docs.moodle.org/en/Moodle_XML_format for a description of the format.
38 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41 class qformat_xml extends qformat_default {
43 public function provide_import() {
44 return true;
47 public function provide_export() {
48 return true;
51 public function mime_type() {
52 return 'application/xml';
55 // IMPORT FUNCTIONS START HERE
57 /**
58 * Translate human readable format name
59 * into internal Moodle code number
60 * @param string name format name from xml file
61 * @return int Moodle format code
63 protected function trans_format($name) {
64 $name = trim($name);
66 if ($name == 'moodle_auto_format') {
67 return FORMAT_MOODLE;
68 } else if ($name == 'html') {
69 return FORMAT_HTML;
70 } else if ($name == 'plain_text') {
71 return FORMAT_PLAIN;
72 } else if ($name == 'wiki_like') {
73 return FORMAT_WIKI;
74 } else if ($name == 'markdown') {
75 return FORMAT_MARKDOWN;
76 } else {
77 return 0; // or maybe warning required
81 /**
82 * Translate human readable single answer option
83 * to internal code number
84 * @param string name true/false
85 * @return int internal code number
87 public function trans_single($name) {
88 $name = trim($name);
89 if ($name == "false" || !$name) {
90 return 0;
91 } else {
92 return 1;
96 /**
97 * process text string from xml file
98 * @param array $text bit of xml tree after ['text']
99 * @return string processed text.
101 public function import_text($text) {
102 // quick sanity check
103 if (empty($text)) {
104 return '';
106 $data = $text[0]['#'];
107 return trim($data);
111 * return the value of a node, given a path to the node
112 * if it doesn't exist return the default value
113 * @param array xml data to read
114 * @param array path path to node expressed as array
115 * @param mixed default
116 * @param bool istext process as text
117 * @param string error if set value must exist, return false and issue message if not
118 * @return mixed value
120 public function getpath($xml, $path, $default, $istext=false, $error='') {
121 foreach ($path as $index) {
122 if (!isset($xml[$index])) {
123 if (!empty($error)) {
124 $this->error($error);
125 return false;
126 } else {
127 return $default;
131 $xml = $xml[$index];
134 if ($istext) {
135 if (!is_string($xml)) {
136 $this->error(get_string('invalidxml', 'qformat_xml'));
138 $xml = trim($xml);
141 return $xml;
146 * import parts of question common to all types
147 * @param $question array question question array from xml tree
148 * @return object question object
150 public function import_headers($question) {
151 global $CFG;
153 // get some error strings
154 $error_noname = get_string('xmlimportnoname', 'qformat_xml');
155 $error_noquestion = get_string('xmlimportnoquestion', 'qformat_xml');
157 // this routine initialises the question object
158 $qo = $this->defaultquestion();
160 // Question name
161 $qo->name = $this->getpath($question,
162 array('#', 'name', 0, '#', 'text', 0, '#'), '', true,
163 get_string('xmlimportnoname', 'qformat_xml'));
164 $qo->questiontext = $this->getpath($question,
165 array('#', 'questiontext', 0, '#', 'text', 0, '#'), '', true);
166 $qo->questiontextformat = $this->trans_format($this->getpath(
167 $question, array('#', 'questiontext', 0, '@', 'format'), ''));
169 $qo->questiontextfiles = $this->import_files($this->getpath($question,
170 array('#', 'questiontext', 0, '#', 'file'), array(), false));
172 // Backwards compatibility, deal with the old image tag.
173 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
174 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
175 if ($filedata && $filename) {
176 $data = new stdClass();
177 $data->content = $filedata;
178 $data->encoding = 'base64';
179 $data->name = $filename;
180 $qo->questiontextfiles[] = $data;
181 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
184 // restore files in generalfeedback
185 $qo->generalfeedback = $this->getpath($question,
186 array('#', 'generalfeedback', 0, '#', 'text', 0, '#'), $qo->generalfeedback, true);
187 $qo->generalfeedbackfiles = array();
188 $qo->generalfeedbackformat = $this->trans_format($this->getpath($question,
189 array('#', 'generalfeedback', 0, '@', 'format'), 'moodle_auto_format'));
190 $qo->generalfeedbackfiles = $this->import_files($this->getpath($question,
191 array('#', 'generalfeedback', 0, '#', 'file'), array(), false));
193 $qo->defaultmark = $this->getpath($question,
194 array('#', 'defaultgrade', 0, '#'), $qo->defaultmark);
195 $qo->penalty = $this->getpath($question,
196 array('#', 'penalty', 0, '#'), $qo->penalty);
198 // Fix problematic rounding from old files:
199 if (abs($qo->penalty - 0.3333333) < 0.005) {
200 $qo->penalty = 0.3333333;
203 // Read the question tags.
204 if (!empty($CFG->usetags) && array_key_exists('tags', $question['#'])
205 && !empty($question['#']['tags'][0]['#']['tag'])) {
206 require_once($CFG->dirroot.'/tag/lib.php');
207 $qo->tags = array();
208 foreach ($question['#']['tags'][0]['#']['tag'] as $tagdata) {
209 $qo->tags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true);
213 return $qo;
217 * Import the common parts of a single answer
218 * @param array answer xml tree for single answer
219 * @return object answer object
221 public function import_answer($answer) {
222 $fraction = $this->getpath($answer, array('@', 'fraction'), 0);
223 $answertext = $this->getpath($answer, array('#', 'text', 0, '#'), '', true);
224 $answerformat = $this->trans_format($this->getpath($answer,
225 array('@', 'format'), 'moodle_auto_format'));
226 $answerfiles = $this->import_files($this->getpath($answer,
227 array('#', 'file'), array()));
229 $feedbacktext = $this->getpath($answer,
230 array('#', 'feedback', 0, '#', 'text', 0, '#'), '', true);
231 $feedbackformat = $this->trans_format($this->getpath($answer,
232 array('#', 'feedback', 0, '@', 'format'), 'moodle_auto_format'));
233 $feedbackfiles = $this->import_files($this->getpath($answer,
234 array('#', 'feedback', 0, '#', 'file'), array()));
236 $ans = new stdClass();
238 $ans->answer = array();
239 $ans->answer['text'] = $answertext;
240 $ans->answer['format'] = $answerformat;
241 $ans->answer['files'] = $answerfiles;
243 $ans->feedback = array();
244 $ans->feedback['text'] = $feedbacktext;
245 $ans->feedback['format'] = $feedbackformat;
246 $ans->feedback['files'] = $feedbackfiles;
248 $ans->fraction = $fraction / 100;
249 return $ans;
253 * Import the common overall feedback fields.
254 * @param object $question the part of the XML relating to this question.
255 * @param object $qo the question data to add the fields to.
256 * @param bool $withshownumpartscorrect include the shownumcorrect field.
258 public function import_combined_feedback($qo, $questionxml, $withshownumpartscorrect = false) {
259 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
260 foreach ($fields as $field) {
261 $text = array();
262 $text['text'] = $this->getpath($questionxml,
263 array('#', $field, 0, '#', 'text', 0, '#'), '', true);
264 $text['format'] = $this->trans_format($this->getpath($questionxml,
265 array('#', $field, 0, '@', 'format'), 'moodle_auto_format'));
266 $text['files'] = $this->import_files($this->getpath($questionxml,
267 array('#', $field, 0, '#', 'file'), array(), false));
269 $qo->$field = $text;
272 if ($withshownumpartscorrect) {
273 $qo->shownumcorrect = array_key_exists('shownumcorrect', $questionxml['#']);
275 // Backwards compatibility:
276 if (array_key_exists('correctresponsesfeedback', $questionxml['#'])) {
277 $qo->shownumcorrect = $this->trans_single($this->getpath($questionxml,
278 array('#', 'correctresponsesfeedback', 0, '#'), 1));
284 * Import a question hint
285 * @param array $hintxml hint xml fragment.
286 * @return object hint for storing in the database.
288 public function import_hint($hintxml) {
289 if (array_key_exists('hintcontent', $hintxml['#'])) {
290 // Backwards compatibility:
292 $hint = new stdClass();
293 $hint->hint = array('format' => FORMAT_HTML, 'files' => array());
294 $hint->hint['text'] = $this->getpath($hintxml,
295 array('#', 'hintcontent', 0, '#', 'text', 0, '#'), '', true);
296 $hint->shownumcorrect = $this->getpath($hintxml,
297 array('#', 'statenumberofcorrectresponses', 0, '#'), 0);
298 $hint->clearwrong = $this->getpath($hintxml,
299 array('#', 'clearincorrectresponses', 0, '#'), 0);
300 $hint->options = $this->getpath($hintxml,
301 array('#', 'showfeedbacktoresponses', 0, '#'), 0);
303 return $hint;
306 $hint->hint = $this->getpath($hintxml,
307 array('#', 'text', 0, '#'), '', true);
308 $hinttext = array();
309 $hinttext['text'] = $this->getpath($hintxml,
310 array('#', 'text', 0, '#'), '', true);
311 $hinttext['format'] = $this->trans_format($this->getpath($hintxml,
312 array('@', 'format'), 'moodle_auto_format'));
314 $hinttext['files'] = $this->import_files($this->getpath($hintxml,
315 array('#', 'file'), array(), false));
317 $hint = new stdClass();
318 $hint->hint = $hinttext;
319 $hint->shownumcorrect = array_key_exists('shownumcorrect', $hintxml['#']);
320 $hint->clearwrong = array_key_exists('clearwrong', $hintxml['#']);
321 $hint->options = $this->getpath($hintxml, array('#', 'options', 0, '#'), '', true);
323 return $hint;
327 * Import all the question hints
329 * @param object $qo the question data that is being constructed.
330 * @param array $hintsxml hints xml fragment.
332 public function import_hints($qo, $questionxml, $withparts = false, $withoptions = false) {
333 if (!isset($questionxml['#']['hint'])) {
334 return;
337 foreach ($questionxml['#']['hint'] as $hintxml) {
338 $hint = $this->import_hint($hintxml);
339 $qo->hint[] = $hint->hint;
341 if ($withparts) {
342 $qo->hintshownumcorrect[] = $hint->shownumcorrect;
343 $qo->hintclearwrong[] = $hint->clearwrong;
346 if ($withoptions) {
347 $qo->hintoptions[] = $hint->options;
353 * Import files from a node in the XML.
354 * @param array $xml an array of <file> nodes from the the parsed XML.
355 * @return array of things representing files - in the form that save_question expects.
357 public function import_files($xml) {
358 $files = array();
359 foreach ($xml as $file) {
360 $data = new stdClass();
361 $data->content = $file['#'];
362 $data->encoding = $file['@']['encoding'];
363 $data->name = $file['@']['name'];
364 $files[] = $data;
366 return $files;
370 * import multiple choice question
371 * @param array question question array from xml tree
372 * @return object question object
374 public function import_multichoice($question) {
375 // get common parts
376 $qo = $this->import_headers($question);
378 // 'header' parts particular to multichoice
379 $qo->qtype = MULTICHOICE;
380 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
381 $qo->single = $this->trans_single($single);
382 $shuffleanswers = $this->getpath($question,
383 array('#', 'shuffleanswers', 0, '#'), 'false');
384 $qo->answernumbering = $this->getpath($question,
385 array('#', 'answernumbering', 0, '#'), 'abc');
386 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
388 // There was a time on the 1.8 branch when it could output an empty
389 // answernumbering tag, so fix up any found.
390 if (empty($qo->answernumbering)) {
391 $qo->answernumbering = 'abc';
394 // Run through the answers
395 $answers = $question['#']['answer'];
396 $acount = 0;
397 foreach ($answers as $answer) {
398 $ans = $this->import_answer($answer);
399 $qo->answer[$acount] = $ans->answer;
400 $qo->fraction[$acount] = $ans->fraction;
401 $qo->feedback[$acount] = $ans->feedback;
402 ++$acount;
405 $this->import_combined_feedback($qo, $question, true);
406 $this->import_hints($qo, $question, true);
408 return $qo;
412 * Import cloze type question
413 * @param array question question array from xml tree
414 * @return object question object
416 public function import_multianswer($questions) {
417 $questiontext = array();
418 $questiontext['text'] = $this->import_text($questions['#']['questiontext'][0]['#']['text']);
419 $questiontext['format'] = '1';
420 $questiontext['itemid'] = '';
421 $qo = qtype_multianswer_extract_question($questiontext);
423 // 'header' parts particular to multianswer
424 $qo->qtype = MULTIANSWER;
425 $qo->course = $this->course;
426 $qo->generalfeedback = '';
427 // restore files in generalfeedback
428 $qo->generalfeedback = $this->getpath($questions,
429 array('#', 'generalfeedback', 0, '#', 'text', 0, '#'), $qo->generalfeedback, true);
430 $qo->generalfeedbackformat = $this->trans_format($this->getpath($questions,
431 array('#', 'generalfeedback', 0, '@', 'format'), 'moodle_auto_format'));
432 $qo->generalfeedbackfiles = $this->import_files($this->getpath($questions,
433 array('#', 'generalfeedback', 0, '#', 'file'), array(), false));
435 if (!empty($questions)) {
436 $qo->name = $this->import_text($questions['#']['name'][0]['#']['text']);
438 $qo->questiontext = $qo->questiontext['text'];
439 $qo->questiontextformat = '';
441 $this->import_hints($qo, $question, true);
443 return $qo;
447 * Import true/false type question
448 * @param array question question array from xml tree
449 * @return object question object
451 public function import_truefalse($question) {
452 // get common parts
453 global $OUTPUT;
454 $qo = $this->import_headers($question);
456 // 'header' parts particular to true/false
457 $qo->qtype = TRUEFALSE;
459 // In the past, it used to be assumed that the two answers were in the file
460 // true first, then false. Howevever that was not always true. Now, we
461 // try to match on the answer text, but in old exports, this will be a localised
462 // string, so if we don't find true or false, we fall back to the old system.
463 $first = true;
464 $warning = false;
465 foreach ($question['#']['answer'] as $answer) {
466 $answertext = $this->getpath($answer,
467 array('#', 'text', 0, '#'), '', true);
468 $feedback = $this->getpath($answer,
469 array('#', 'feedback', 0, '#', 'text', 0, '#'), '', true);
470 $feedbackformat = $this->getpath($answer,
471 array('#', 'feedback', 0, '@', 'format'), 'moodle_auto_format');
472 $feedbackfiles = $this->getpath($answer,
473 array('#', 'feedback', 0, '#', 'file'), array());
474 $files = array();
475 foreach ($feedbackfiles as $file) {
476 $data = new stdClass();
477 $data->content = $file['#'];
478 $data->encoding = $file['@']['encoding'];
479 $data->name = $file['@']['name'];
480 $files[] = $data;
482 if ($answertext != 'true' && $answertext != 'false') {
483 // Old style file, assume order is true/false.
484 $warning = true;
485 if ($first) {
486 $answertext = 'true';
487 } else {
488 $answertext = 'false';
492 if ($answertext == 'true') {
493 $qo->answer = ($answer['@']['fraction'] == 100);
494 $qo->correctanswer = $qo->answer;
495 $qo->feedbacktrue = array();
496 $qo->feedbacktrue['text'] = $feedback;
497 $qo->feedbacktrue['format'] = $this->trans_format($feedbackformat);
498 $qo->feedbacktrue['files'] = $files;
499 } else {
500 $qo->answer = ($answer['@']['fraction'] != 100);
501 $qo->correctanswer = $qo->answer;
502 $qo->feedbackfalse = array();
503 $qo->feedbackfalse['text'] = $feedback;
504 $qo->feedbackfalse['format'] = $this->trans_format($feedbackformat);
505 $qo->feedbackfalse['files'] = $files;
507 $first = false;
510 if ($warning) {
511 $a = new stdClass();
512 $a->questiontext = $qo->questiontext;
513 $a->answer = get_string($qo->correctanswer ? 'true' : 'false', 'qtype_truefalse');
514 echo $OUTPUT->notification(get_string('truefalseimporterror', 'qformat_xml', $a));
517 $this->import_hints($qo, $question);
519 return $qo;
523 * Import short answer type question
524 * @param array question question array from xml tree
525 * @return object question object
527 public function import_shortanswer($question) {
528 // get common parts
529 $qo = $this->import_headers($question);
531 // header parts particular to shortanswer
532 $qo->qtype = SHORTANSWER;
534 // get usecase
535 $qo->usecase = $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase);
537 // Run through the answers
538 $answers = $question['#']['answer'];
539 $acount = 0;
540 foreach ($answers as $answer) {
541 $ans = $this->import_answer($answer);
542 $qo->answer[$acount] = $ans->answer['text'];
543 $qo->fraction[$acount] = $ans->fraction;
544 $qo->feedback[$acount] = $ans->feedback;
545 ++$acount;
548 $this->import_hints($qo, $question);
550 return $qo;
554 * Import description type question
555 * @param array question question array from xml tree
556 * @return object question object
558 public function import_description($question) {
559 // get common parts
560 $qo = $this->import_headers($question);
561 // header parts particular to shortanswer
562 $qo->qtype = DESCRIPTION;
563 $qo->defaultmark = 0;
564 $qo->length = 0;
565 return $qo;
569 * Import numerical type question
570 * @param array question question array from xml tree
571 * @return object question object
573 public function import_numerical($question) {
574 // get common parts
575 $qo = $this->import_headers($question);
577 // header parts particular to numerical
578 $qo->qtype = NUMERICAL;
580 // get answers array
581 $answers = $question['#']['answer'];
582 $qo->answer = array();
583 $qo->feedback = array();
584 $qo->fraction = array();
585 $qo->tolerance = array();
586 foreach ($answers as $answer) {
587 // answer outside of <text> is deprecated
588 $obj = $this->import_answer($answer);
589 $qo->answer[] = $obj->answer['text'];
590 if (empty($qo->answer)) {
591 $qo->answer = '*';
593 $qo->feedback[] = $obj->feedback;
594 $qo->tolerance[] = $this->getpath($answer, array('#', 'tolerance', 0, '#'), 0);
596 // fraction as a tag is deprecated
597 $fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
598 $qo->fraction[] = $this->getpath($answer,
599 array('#', 'fraction', 0, '#'), $fraction); // deprecated
602 // Get the units array
603 $qo->unit = array();
604 $units = $this->getpath($question, array('#', 'units', 0, '#', 'unit'), array());
605 if (!empty($units)) {
606 $qo->multiplier = array();
607 foreach ($units as $unit) {
608 $qo->multiplier[] = $this->getpath($unit, array('#', 'multiplier', 0, '#'), 1);
609 $qo->unit[] = $this->getpath($unit, array('#', 'unit_name', 0, '#'), '', true);
612 $qo->unitgradingtype = $this->getpath($question, array('#', 'unitgradingtype', 0, '#'), 0);
613 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0);
614 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
615 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
616 $qo->instructions['text'] = '';
617 $qo->instructions['format'] = FORMAT_HTML;
618 $instructions = $this->getpath($question, array('#', 'instructions'), array());
619 if (!empty($instructions)) {
620 $qo->instructions = array();
621 $qo->instructions['text'] = $this->getpath($instructions,
622 array('0', '#', 'text', '0', '#'), '', true);
623 $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
624 array('0', '@', 'format'), 'moodle_auto_format'));
625 $qo->instructions['files'] = $this->import_files($this->getpath(
626 $instructions, array('0', '#', 'file'), array()));
629 $this->import_hints($qo, $question);
631 return $qo;
635 * Import matching type question
636 * @param array question question array from xml tree
637 * @return object question object
639 public function import_matching($question) {
640 // get common parts
641 $qo = $this->import_headers($question);
643 // header parts particular to matching
644 $qo->qtype = MATCH;
645 $qo->shuffleanswers = $this->trans_single($this->getpath($question,
646 array('#', 'shuffleanswers', 0, '#'), 1));
648 // run through subquestions
649 $qo->subquestions = array();
650 $qo->subanswers = array();
651 foreach ($question['#']['subquestion'] as $subqxml) {
652 $subquestion = array();
653 $subquestion['text'] = $this->getpath($subqxml, array('#', 'text', 0, '#'), '', true);
654 $subquestion['format'] = $this->trans_format($this->getpath($subqxml,
655 array('@', 'format'), 'moodle_auto_format'));
656 $subquestion['files'] = $this->import_files($this->getpath($subqxml,
657 array('#', 'file'), array()));
659 $qo->subquestions[] = $subquestion;
660 $answers = $this->getpath($subqxml, array('#', 'answer'), array());
661 $qo->subanswers[] = $this->getpath($subqxml,
662 array('#', 'answer', 0, '#', 'text', 0, '#'), '', true);
665 $this->import_combined_feedback($qo, $question, true);
666 $this->import_hints($qo, $question, true);
668 return $qo;
672 * Import essay type question
673 * @param array question question array from xml tree
674 * @return object question object
676 public function import_essay($question) {
677 // get common parts
678 $qo = $this->import_headers($question);
680 // header parts particular to essay
681 $qo->qtype = ESSAY;
683 $answers = $this->getpath($question, array('#', 'answer'), null);
684 if ($answers) {
685 $answer = array_pop($answers);
686 $answer = $this->import_answer($answer);
687 // get feedback
688 $qo->feedback = $answer->feedback;
689 } else {
690 $qo->feedback = array('text' => '', 'format' => FORMAT_MOODLE, 'files' => array());
693 // get fraction - <fraction> tag is deprecated
694 $qo->fraction = $this->getpath($question, array('@', 'fraction'), 0) / 100;
695 $qo->fraction = $this->getpath($question, array('#', 'fraction', 0, '#'), $qo->fraction);
697 return $qo;
701 * Import a calculated question
702 * @param object $question the imported XML data.
704 public function import_calculated($question) {
706 // get common parts
707 $qo = $this->import_headers($question);
709 // header parts particular to calculated
710 $qo->qtype = CALCULATED;
711 $qo->synchronize = $this->getpath($question, array('#', 'synchronize', 0, '#'), 0);
712 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
713 $qo->single = $this->trans_single($single);
714 $shuffleanswers = $this->getpath($question, array('#', 'shuffleanswers', 0, '#'), 'false');
715 $qo->answernumbering = $this->getpath($question,
716 array('#', 'answernumbering', 0, '#'), 'abc');
717 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
719 $qo->correctfeedback = array();
720 $qo->correctfeedback['text'] = $this->getpath(
721 $question, array('#', 'correctfeedback', 0, '#', 'text', 0, '#'), '', true);
722 $qo->correctfeedback['format'] = $this->trans_format($this->getpath(
723 $question, array('#', 'correctfeedback', 0, '@', 'formath'), 'moodle_auto_format'));
724 $qo->correctfeedback['files'] = $this->import_files($this->getpath(
725 $question, array('#', 'correctfeedback', '0', '#', 'file'), array()));
727 $qo->partiallycorrectfeedback = array();
728 $qo->partiallycorrectfeedback['text'] = $this->getpath($question,
729 array('#', 'partiallycorrectfeedback', 0, '#', 'text', 0, '#'), '', true);
730 $qo->partiallycorrectfeedback['format'] = $this->trans_format(
731 $this->getpath($question, array('#', 'partiallycorrectfeedback', 0, '@', 'format'),
732 'moodle_auto_format'));
733 $qo->partiallycorrectfeedback['files'] = $this->import_files($this->getpath(
734 $question, array('#', 'partiallycorrectfeedback', '0', '#', 'file'), array()));
736 $qo->incorrectfeedback = array();
737 $qo->incorrectfeedback['text'] = $this->getpath($question,
738 array('#', 'incorrectfeedback', 0, '#', 'text', 0, '#'), '', true);
739 $qo->incorrectfeedback['format'] = $this->trans_format($this->getpath($question,
740 array('#', 'incorrectfeedback', 0, '@', 'format'), 'moodle_auto_format'));
741 $qo->incorrectfeedback['files'] = $this->import_files($this->getpath($question,
742 array('#', 'incorrectfeedback', '0', '#', 'file'), array()));
744 $qo->unitgradingtype = $this->getpath($question,
745 array('#', 'unitgradingtype', 0, '#'), 0);
746 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0);
747 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
748 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
749 $qo->instructions = $this->getpath($question,
750 array('#', 'instructions', 0, '#', 'text', 0, '#'), '', true);
751 if (!empty($instructions)) {
752 $qo->instructions = array();
753 $qo->instructions['text'] = $this->getpath($instructions,
754 array('0', '#', 'text', '0', '#'), '', true);
755 $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
756 array('0', '@', 'format'), 'moodle_auto_format'));
757 $qo->instructions['files'] = $this->import_files($this->getpath($instructions,
758 array('0', '#', 'file'), array()));
761 // get answers array
762 $answers = $question['#']['answer'];
763 $qo->answers = array();
764 $qo->feedback = array();
765 $qo->fraction = array();
766 $qo->tolerance = array();
767 $qo->tolerancetype = array();
768 $qo->correctanswerformat = array();
769 $qo->correctanswerlength = array();
770 $qo->feedback = array();
771 foreach ($answers as $answer) {
772 $ans = $this->import_answer($answer);
773 // answer outside of <text> is deprecated
774 if (empty($ans->answer['text'])) {
775 $ans->answer['text'] = '*';
777 $qo->answers[] = $ans->answer;
778 $qo->feedback[] = $ans->feedback;
779 $qo->tolerance[] = $answer['#']['tolerance'][0]['#'];
780 // fraction as a tag is deprecated
781 if (!empty($answer['#']['fraction'][0]['#'])) {
782 $qo->fraction[] = $answer['#']['fraction'][0]['#'];
783 } else {
784 $qo->fraction[] = $answer['@']['fraction'] / 100;
786 $qo->tolerancetype[] = $answer['#']['tolerancetype'][0]['#'];
787 $qo->correctanswerformat[] = $answer['#']['correctanswerformat'][0]['#'];
788 $qo->correctanswerlength[] = $answer['#']['correctanswerlength'][0]['#'];
790 // get units array
791 $qo->unit = array();
792 if (isset($question['#']['units'][0]['#']['unit'])) {
793 $units = $question['#']['units'][0]['#']['unit'];
794 $qo->multiplier = array();
795 foreach ($units as $unit) {
796 $qo->multiplier[] = $unit['#']['multiplier'][0]['#'];
797 $qo->unit[] = $unit['#']['unit_name'][0]['#'];
800 $instructions = $this->getpath($question, array('#', 'instructions'), array());
801 if (!empty($instructions)) {
802 $qo->instructions = array();
803 $qo->instructions['text'] = $this->getpath($instructions,
804 array('0', '#', 'text', '0', '#'), '', true);
805 $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
806 array('0', '@', 'format'), 'moodle_auto_format'));
807 $qo->instructions['files'] = $this->import_files($this->getpath($instructions,
808 array('0', '#', 'file'), array()));
810 $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition'];
811 $qo->dataset = array();
812 $qo->datasetindex= 0;
813 foreach ($datasets as $dataset) {
814 $qo->datasetindex++;
815 $qo->dataset[$qo->datasetindex] = new stdClass();
816 $qo->dataset[$qo->datasetindex]->status =
817 $this->import_text($dataset['#']['status'][0]['#']['text']);
818 $qo->dataset[$qo->datasetindex]->name =
819 $this->import_text($dataset['#']['name'][0]['#']['text']);
820 $qo->dataset[$qo->datasetindex]->type =
821 $dataset['#']['type'][0]['#'];
822 $qo->dataset[$qo->datasetindex]->distribution =
823 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
824 $qo->dataset[$qo->datasetindex]->max =
825 $this->import_text($dataset['#']['maximum'][0]['#']['text']);
826 $qo->dataset[$qo->datasetindex]->min =
827 $this->import_text($dataset['#']['minimum'][0]['#']['text']);
828 $qo->dataset[$qo->datasetindex]->length =
829 $this->import_text($dataset['#']['decimals'][0]['#']['text']);
830 $qo->dataset[$qo->datasetindex]->distribution =
831 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
832 $qo->dataset[$qo->datasetindex]->itemcount = $dataset['#']['itemcount'][0]['#'];
833 $qo->dataset[$qo->datasetindex]->datasetitem = array();
834 $qo->dataset[$qo->datasetindex]->itemindex = 0;
835 $qo->dataset[$qo->datasetindex]->number_of_items =
836 $dataset['#']['number_of_items'][0]['#'];
837 $datasetitems = $dataset['#']['dataset_items'][0]['#']['dataset_item'];
838 foreach ($datasetitems as $datasetitem) {
839 $qo->dataset[$qo->datasetindex]->itemindex++;
840 $qo->dataset[$qo->datasetindex]->datasetitem[
841 $qo->dataset[$qo->datasetindex]->itemindex] = new stdClass();
842 $qo->dataset[$qo->datasetindex]->datasetitem[
843 $qo->dataset[$qo->datasetindex]->itemindex]->itemnumber =
844 $datasetitem['#']['number'][0]['#'];
845 $qo->dataset[$qo->datasetindex]->datasetitem[
846 $qo->dataset[$qo->datasetindex]->itemindex]->value =
847 $datasetitem['#']['value'][0]['#'];
851 $this->import_hints($qo, $question);
853 return $qo;
857 * This is not a real question type. It's a dummy type used to specify the
858 * import category. The format is:
859 * <question type="category">
860 * <category>tom/dick/harry</category>
861 * </question>
863 protected function import_category($question) {
864 $qo = new stdClass();
865 $qo->qtype = 'category';
866 $qo->category = $this->import_text($question['#']['category'][0]['#']['text']);
867 return $qo;
871 * Parse the array of lines into an array of questions
872 * this *could* burn memory - but it won't happen that much
873 * so fingers crossed!
874 * @param array of lines from the input file.
875 * @return array (of objects) question objects.
877 protected function readquestions($lines) {
878 // We just need it as one big string
879 $text = implode($lines, ' ');
880 unset($lines);
882 // This converts xml to big nasty data structure
883 // the 0 means keep white space as it is (important for markdown format)
884 try {
885 $xml = xmlize($text, 0, 'UTF-8', true);
886 } catch (xml_format_exception $e) {
887 $this->error($e->getMessage(), '');
888 return false;
890 // Set up array to hold all our questions
891 $questions = array();
893 // Iterate through questions
894 foreach ($xml['quiz']['#']['question'] as $question) {
895 $questiontype = $question['@']['type'];
897 if ($questiontype == 'multichoice') {
898 $qo = $this->import_multichoice($question);
899 } else if ($questiontype == 'truefalse') {
900 $qo = $this->import_truefalse($question);
901 } else if ($questiontype == 'shortanswer') {
902 $qo = $this->import_shortanswer($question);
903 } else if ($questiontype == 'numerical') {
904 $qo = $this->import_numerical($question);
905 } else if ($questiontype == 'description') {
906 $qo = $this->import_description($question);
907 } else if ($questiontype == 'matching') {
908 $qo = $this->import_matching($question);
909 } else if ($questiontype == 'cloze') {
910 $qo = $this->import_multianswer($question);
911 } else if ($questiontype == 'essay') {
912 $qo = $this->import_essay($question);
913 } else if ($questiontype == 'calculated') {
914 $qo = $this->import_calculated($question);
915 } else if ($questiontype == 'category') {
916 $qo = $this->import_category($question);
918 } else {
919 // Not a type we handle ourselves. See if the question type wants
920 // to handle it.
921 if (!$qo = $this->try_importing_using_qtypes(
922 $question, null, null, $questiontype)) {
923 $this->error(get_string('xmltypeunsupported', 'qformat_xml', $questiontype));
924 $qo = null;
928 // Stick the result in the $questions array
929 if ($qo) {
930 $questions[] = $qo;
933 return $questions;
936 // EXPORT FUNCTIONS START HERE
938 public function export_file_extension() {
939 return '.xml';
943 * Turn the internal question code into a human readable form
944 * (The code used to be numeric, but this remains as some of
945 * the names don't match the new internal format)
946 * @param mixed $typeid Internal code
947 * @return string question type string
949 protected function get_qtype($typeid) {
950 switch($typeid) {
951 case TRUEFALSE:
952 return 'truefalse';
953 case MULTICHOICE:
954 return 'multichoice';
955 case SHORTANSWER:
956 return 'shortanswer';
957 case NUMERICAL:
958 return 'numerical';
959 case MATCH:
960 return 'matching';
961 case DESCRIPTION:
962 return 'description';
963 case MULTIANSWER:
964 return 'cloze';
965 case ESSAY:
966 return 'essay';
967 case CALCULATED:
968 return 'calculated';
969 default:
970 return false;
975 * Convert internal Moodle text format code into
976 * human readable form
977 * @param int id internal code
978 * @return string format text
980 protected function get_format($id) {
981 switch($id) {
982 case FORMAT_MOODLE:
983 return 'moodle_auto_format';
984 case FORMAT_HTML:
985 return 'html';
986 case FORMAT_PLAIN:
987 return 'plain_text';
988 case FORMAT_WIKI:
989 return 'wiki_like';
990 case FORMAT_MARKDOWN:
991 return 'markdown';
992 default:
993 return 'unknown';
998 * Convert internal single question code into
999 * human readable form
1000 * @param int id single question code
1001 * @return string single question string
1003 public function get_single($id) {
1004 switch($id) {
1005 case 0:
1006 return 'false';
1007 case 1:
1008 return 'true';
1009 default:
1010 return 'unknown';
1015 * Generates <text></text> tags, processing raw text therein
1016 * @param string $raw the content to output.
1017 * @param int $indent the current indent level.
1018 * @param bool $short stick it on one line.
1019 * @return string formatted text.
1021 public function writetext($raw, $indent = 0, $short = true) {
1022 $indent = str_repeat(' ', $indent);
1024 // if required add CDATA tags
1025 if (!empty($raw) && htmlspecialchars($raw) != $raw) {
1026 $raw = "<![CDATA[$raw]]>";
1029 if ($short) {
1030 $xml = "$indent<text>$raw</text>\n";
1031 } else {
1032 $xml = "$indent<text>\n$raw\n$indent</text>\n";
1035 return $xml;
1038 protected function presave_process($content) {
1039 // Override to allow us to add xml headers and footers
1040 return '<?xml version="1.0" encoding="UTF-8"?>
1041 <quiz>
1042 ' . $content . '</quiz>';
1046 * Turns question into an xml segment
1047 * @param object $question the question data.
1048 * @return string xml segment
1050 public function writequestion($question) {
1051 global $CFG, $OUTPUT;
1053 $fs = get_file_storage();
1054 $contextid = $question->contextid;
1055 // Get files used by the questiontext.
1056 $question->questiontextfiles = $fs->get_area_files(
1057 $contextid, 'question', 'questiontext', $question->id);
1058 // Get files used by the generalfeedback.
1059 $question->generalfeedbackfiles = $fs->get_area_files(
1060 $contextid, 'question', 'generalfeedback', $question->id);
1061 if (!empty($question->options->answers)) {
1062 foreach ($question->options->answers as $answer) {
1063 $answer->feedbackfiles = $fs->get_area_files(
1064 $contextid, 'question', 'answerfeedback', $answer->id);
1068 $expout = '';
1070 // Add a comment linking this to the original question id.
1071 $expout .= "<!-- question: $question->id -->\n";
1073 // Check question type
1074 if (!$questiontype = $this->get_qtype($question->qtype)) {
1075 // must be a plugin then, so just accept the name supplied
1076 $questiontype = $question->qtype;
1079 // add opening tag
1080 // generates specific header for Cloze and category type question
1081 if ($question->qtype == 'category') {
1082 $categorypath = $this->writetext($question->category);
1083 $expout .= " <question type=\"category\">\n";
1084 $expout .= " <category>\n";
1085 $expout .= " $categorypath\n";
1086 $expout .= " </category>\n";
1087 $expout .= " </question>\n";
1088 return $expout;
1090 } else if ($question->qtype != MULTIANSWER) {
1091 // for all question types except Close
1092 $name_text = $this->writetext($question->name, 3);
1094 $expout .= " <question type=\"$questiontype\">\n";
1095 $expout .= " <name>\n";
1096 $expout .= $name_text;
1097 $expout .= " </name>\n";
1098 $expout .= " <questiontext {$this->format($question->questiontextformat)}>\n";
1099 $expout .= $this->writetext($question->questiontext, 3);
1100 $expout .= $this->writefiles($question->questiontextfiles);
1101 $expout .= " </questiontext>\n";
1102 $expout .= " <generalfeedback {$this->format($question->generalfeedbackformat)}>\n";
1103 $expout .= $this->writetext($question->generalfeedback, 3);
1104 $expout .= $this->writefiles($question->generalfeedbackfiles);
1105 $expout .= " </generalfeedback>\n";
1106 $expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
1107 $expout .= " <penalty>{$question->penalty}</penalty>\n";
1108 $expout .= " <hidden>{$question->hidden}</hidden>\n";
1110 } else {
1111 // for Cloze type only
1112 $name_text = $this->writetext($question->name);
1113 $question_text = $this->writetext($question->questiontext);
1114 $generalfeedback = $this->writetext($question->generalfeedback);
1115 $expout .= " <question type=\"$questiontype\">\n";
1116 $expout .= " <name>\n";
1117 $expout .= $name_text;
1118 $expout .= " </name>\n";
1119 $expout .= " <questiontext>\n";
1120 $expout .= $this->writetext($question->questiontext, 3);
1121 $expout .= $this->writefiles($question->questiontextfiles);
1122 $expout .= " </questiontext>\n";
1123 $expout .= " <generalfeedback>\n";
1124 $expout .= $this->writetext($question->generalfeedback, 3);
1125 $expout .= $this->writefiles($question->generalfeedbackfiles);
1126 $expout .= " </generalfeedback>\n";
1129 // output depends on question type
1130 switch($question->qtype) {
1131 case 'category':
1132 // not a qtype really - dummy used for category switching
1133 break;
1135 case 'truefalse':
1136 $trueanswer = $question->options->answers[$question->options->trueanswer];
1137 $trueanswer->answer = 'true';
1138 $expout .= $this->write_answer($trueanswer);
1140 $falseanswer = $question->options->answers[$question->options->falseanswer];
1141 $falseanswer->answer = 'false';
1142 $expout .= $this->write_answer($falseanswer);
1143 break;
1145 case 'multichoice':
1146 $expout .= " <single>" . $this->get_single($question->options->single) .
1147 "</single>\n";
1148 $expout .= " <shuffleanswers>" .
1149 $this->get_single($question->options->shuffleanswers) .
1150 "</shuffleanswers>\n";
1151 $expout .= " <answernumbering>" . $question->options->answernumbering .
1152 "</answernumbering>\n";
1153 $expout .= $this->write_combined_feedback($question->options);
1154 $expout .= $this->write_answers($question->options->answers);
1155 break;
1157 case 'shortanswer':
1158 $expout .= " <usecase>{$question->options->usecase}</usecase>\n";
1159 $expout .= $this->write_answers($question->options->answers);
1160 break;
1162 case 'numerical':
1163 foreach ($question->options->answers as $answer) {
1164 $expout .= $this->write_answer($answer,
1165 " <tolerance>$answer->tolerance</tolerance>\n");
1168 $units = $question->options->units;
1169 if (count($units)) {
1170 $expout .= "<units>\n";
1171 foreach ($units as $unit) {
1172 $expout .= " <unit>\n";
1173 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1174 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1175 $expout .= " </unit>\n";
1177 $expout .= "</units>\n";
1179 if (isset($question->options->unitgradingtype)) {
1180 $expout .= " <unitgradingtype>" . $question->options->unitgradingtype .
1181 "</unitgradingtype>\n";
1183 if (isset($question->options->unitpenalty)) {
1184 $expout .= " <unitpenalty>{$question->options->unitpenalty}</unitpenalty>\n";
1186 if (isset($question->options->showunits)) {
1187 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1189 if (isset($question->options->unitsleft)) {
1190 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1192 if (!empty($question->options->instructionsformat)) {
1193 $files = $fs->get_area_files($contextid, 'qtype_numerical',
1194 'instruction', $question->id);
1195 $expout .= " <instructions " .
1196 $this->format($question->options->instructionsformat) . ">\n";
1197 $expout .= $this->writetext($question->options->instructions, 3);
1198 $expout .= $this->writefiles($files);
1199 $expout .= " </instructions>\n";
1201 break;
1203 case 'match':
1204 $expout .= " <shuffleanswers>" .
1205 $this->get_single($question->options->shuffleanswers) .
1206 "</shuffleanswers>\n";
1207 $expout .= $this->write_combined_feedback($question->options);
1208 foreach ($question->options->subquestions as $subquestion) {
1209 $files = $fs->get_area_files($contextid, 'qtype_match',
1210 'subquestion', $subquestion->id);
1211 $expout .= " <subquestion " .
1212 $this->format($subquestion->questiontextformat) . ">\n";
1213 $expout .= $this->writetext($subquestion->questiontext, 3);
1214 $expout .= $this->writefiles($files);
1215 $expout .= " <answer>\n";
1216 $expout .= $this->writetext($subquestion->answertext, 4);
1217 $expout .= " </answer>\n";
1218 $expout .= " </subquestion>\n";
1220 break;
1222 case 'description':
1223 // Nothing else to do.
1224 break;
1226 case 'multianswer':
1227 $acount = 1;
1228 foreach ($question->options->questions as $question) {
1229 $thispattern = "{#".$acount."}";
1230 $thisreplace = $question->questiontext;
1231 $expout = preg_replace("~$thispattern~", $thisreplace, $expout);
1232 $acount++;
1234 break;
1236 case 'essay':
1237 // Nothing else to do.
1238 break;
1240 case 'calculated':
1241 case 'calculatedsimple':
1242 case 'calculatedmulti':
1243 $expout .= " <synchronize>{$question->options->synchronize}</synchronize>\n";
1244 $expout .= " <single>{$question->options->single}</single>\n";
1245 $expout .= " <answernumbering>" . $question->options->answernumbering .
1246 "</answernumbering>\n";
1247 $expout .= " <shuffleanswers>" .
1248 $this->writetext($question->options->shuffleanswers, 3) .
1249 "</shuffleanswers>\n";
1251 $component = 'qtype_' . $question->qtype;
1252 $files = $fs->get_area_files($contextid, $component,
1253 'correctfeedback', $question->id);
1254 $expout .= " <correctfeedback>\n";
1255 $expout .= $this->writetext($question->options->correctfeedback, 3);
1256 $expout .= $this->writefiles($files);
1257 $expout .= " </correctfeedback>\n";
1259 $files = $fs->get_area_files($contextid, $component,
1260 'partiallycorrectfeedback', $question->id);
1261 $expout .= " <partiallycorrectfeedback>\n";
1262 $expout .= $this->writetext($question->options->partiallycorrectfeedback, 3);
1263 $expout .= $this->writefiles($files);
1264 $expout .= " </partiallycorrectfeedback>\n";
1266 $files = $fs->get_area_files($contextid, $component,
1267 'incorrectfeedback', $question->id);
1268 $expout .= " <incorrectfeedback>\n";
1269 $expout .= $this->writetext($question->options->incorrectfeedback, 3);
1270 $expout .= $this->writefiles($files);
1271 $expout .= " </incorrectfeedback>\n";
1273 foreach ($question->options->answers as $answer) {
1274 $percent = 100 * $answer->fraction;
1275 $expout .= "<answer fraction=\"$percent\">\n";
1276 // "<text/>" tags are an added feature, old files won't have them
1277 $expout .= " <text>{$answer->answer}</text>\n";
1278 $expout .= " <tolerance>{$answer->tolerance}</tolerance>\n";
1279 $expout .= " <tolerancetype>{$answer->tolerancetype}</tolerancetype>\n";
1280 $expout .= " <correctanswerformat>" .
1281 $answer->correctanswerformat . "</correctanswerformat>\n";
1282 $expout .= " <correctanswerlength>" .
1283 $answer->correctanswerlength . "</correctanswerlength>\n";
1284 $expout .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1285 $files = $fs->get_area_files($contextid, $component,
1286 'instruction', $question->id);
1287 $expout .= $this->writetext($answer->feedback);
1288 $expout .= $this->writefiles($answer->feedbackfiles);
1289 $expout .= " </feedback>\n";
1290 $expout .= "</answer>\n";
1292 if (isset($question->options->unitgradingtype)) {
1293 $expout .= " <unitgradingtype>" .
1294 $question->options->unitgradingtype . "</unitgradingtype>\n";
1296 if (isset($question->options->unitpenalty)) {
1297 $expout .= " <unitpenalty>" .
1298 $question->options->unitpenalty . "</unitpenalty>\n";
1300 if (isset($question->options->showunits)) {
1301 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1303 if (isset($question->options->unitsleft)) {
1304 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1307 if (isset($question->options->instructionsformat)) {
1308 $files = $fs->get_area_files($contextid, $component,
1309 'instruction', $question->id);
1310 $expout .= " <instructions " .
1311 $this->format($question->options->instructionsformat) . ">\n";
1312 $expout .= $this->writetext($question->options->instructions, 3);
1313 $expout .= $this->writefiles($files);
1314 $expout .= " </instructions>\n";
1317 if (isset($question->options->units)) {
1318 $units = $question->options->units;
1319 if (count($units)) {
1320 $expout .= "<units>\n";
1321 foreach ($units as $unit) {
1322 $expout .= " <unit>\n";
1323 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1324 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1325 $expout .= " </unit>\n";
1327 $expout .= "</units>\n";
1331 // The tag $question->export_process has been set so we get all the
1332 // data items in the database from the function
1333 // qtype_calculated::get_question_options calculatedsimple defaults
1334 // to calculated
1335 if (isset($question->options->datasets) && count($question->options->datasets)) {
1336 $expout .= "<dataset_definitions>\n";
1337 foreach ($question->options->datasets as $def) {
1338 $expout .= "<dataset_definition>\n";
1339 $expout .= " <status>".$this->writetext($def->status)."</status>\n";
1340 $expout .= " <name>".$this->writetext($def->name)."</name>\n";
1341 if ($question->qtype == CALCULATED) {
1342 $expout .= " <type>calculated</type>\n";
1343 } else {
1344 $expout .= " <type>calculatedsimple</type>\n";
1346 $expout .= " <distribution>" . $this->writetext($def->distribution) .
1347 "</distribution>\n";
1348 $expout .= " <minimum>" . $this->writetext($def->minimum) .
1349 "</minimum>\n";
1350 $expout .= " <maximum>" . $this->writetext($def->maximum) .
1351 "</maximum>\n";
1352 $expout .= " <decimals>" . $this->writetext($def->decimals) .
1353 "</decimals>\n";
1354 $expout .= " <itemcount>$def->itemcount</itemcount>\n";
1355 if ($def->itemcount > 0) {
1356 $expout .= " <dataset_items>\n";
1357 foreach ($def->items as $item) {
1358 $expout .= " <dataset_item>\n";
1359 $expout .= " <number>".$item->itemnumber."</number>\n";
1360 $expout .= " <value>".$item->value."</value>\n";
1361 $expout .= " </dataset_item>\n";
1363 $expout .= " </dataset_items>\n";
1364 $expout .= " <number_of_items>" . $def->number_of_items .
1365 "</number_of_items>\n";
1367 $expout .= "</dataset_definition>\n";
1369 $expout .= "</dataset_definitions>\n";
1371 break;
1373 default:
1374 // try support by optional plugin
1375 if (!$data = $this->try_exporting_using_qtypes($question->qtype, $question)) {
1376 notify(get_string('unsupportedexport', 'qformat_xml', $question->qtype));
1378 $expout .= $data;
1381 // Output any hints.
1382 $expout .= $this->write_hints($question);
1384 // Write the question tags.
1385 if (!empty($CFG->usetags)) {
1386 require_once($CFG->dirroot.'/tag/lib.php');
1387 $tags = tag_get_tags_array('question', $question->id);
1388 if (!empty($tags)) {
1389 $expout .= " <tags>\n";
1390 foreach ($tags as $tag) {
1391 $expout .= " <tag>" . $this->writetext($tag, 0, true) . "</tag>\n";
1393 $expout .= " </tags>\n";
1397 // close the question tag
1398 $expout .= " </question>\n";
1400 return $expout;
1403 public function write_answers($answers) {
1404 if (empty($answers)) {
1405 return;
1407 $output = '';
1408 foreach ($answers as $answer) {
1409 $output .= $this->write_answer($answer);
1411 return $output;
1414 public function write_answer($answer, $extra = '') {
1415 $percent = $answer->fraction * 100;
1416 $output = '';
1417 $output .= " <answer fraction=\"$percent\" {$this->format($answer->answerformat)}>\n";
1418 $output .= $this->writetext($answer->answer, 3);
1419 $output .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1420 $output .= $this->writetext($answer->feedback, 4);
1421 $output .= $this->writefiles($answer->feedbackfiles);
1422 $output .= " </feedback>\n";
1423 $output .= $extra;
1424 $output .= " </answer>\n";
1425 return $output;
1428 public function write_hints($question) {
1429 if (empty($question->hints)) {
1430 return '';
1433 $output = '';
1434 foreach ($question->hints as $hint) {
1435 $output .= $this->write_hint($hint);
1437 return $output;
1441 * @param unknown_type $format a FORMAT_... constant.
1442 * @return string the attribute to add to an XML tag.
1444 protected function format($format) {
1445 return 'format="' . $this->get_format($format) . '"';
1448 public function write_hint($hint) {
1449 $output = '';
1450 $output .= " <hint {$this->format($hint->hintformat)}>\n";
1451 $output .= ' ' . $this->writetext($hint->hint);
1452 if (!empty($hint->shownumcorrect)) {
1453 $output .= " <shownumcorrect/>\n";
1455 if (!empty($hint->clearwrong)) {
1456 $output .= " <clearwrong/>\n";
1458 if (!empty($hint->options)) {
1459 $output .= ' <options>' . htmlspecialchars($hint->options) . "</options>\n";
1461 $output .= " </hint>\n";
1462 return $output;
1465 public function write_combined_feedback($questionoptions) {
1466 $output = " <correctfeedback {$this->format($questionoptions->correctfeedbackformat)}>
1467 {$this->writetext($questionoptions->correctfeedback)} </correctfeedback>
1468 <partiallycorrectfeedback {$this->format($questionoptions->partiallycorrectfeedbackformat)}>
1469 {$this->writetext($questionoptions->partiallycorrectfeedback)} </partiallycorrectfeedback>
1470 <incorrectfeedback {$this->format($questionoptions->incorrectfeedbackformat)}>
1471 {$this->writetext($questionoptions->incorrectfeedback)} </incorrectfeedback>\n";
1472 if (!empty($questionoptions->shownumcorrect)) {
1473 $output .= " <shownumcorrect/>\n";
1475 return $output;