2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Code for exporting questions as Moodle XML.
22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->libdir . '/xmlize.php');
30 if (!class_exists('qformat_default')) {
31 // This is ugly, but this class is also (ab)used by mod/lesson, which defines
32 // a different base class in mod/lesson/format.php. Thefore, we can only
33 // include the proper base class conditionally like this. (We have to include
34 // the base class like this, otherwise it breaks third-party question types.)
35 // This may be reviewd, and a better fix found one day.
36 require_once($CFG->dirroot . '/question/format.php');
41 * Importer for Moodle XML question format.
43 * See http://docs.moodle.org/en/Moodle_XML_format for a description of the format.
45 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
48 class qformat_xml extends qformat_default {
50 public function provide_import() {
54 public function provide_export() {
58 public function mime_type() {
59 return 'application/xml';
62 // IMPORT FUNCTIONS START HERE
65 * Translate human readable format name
66 * into internal Moodle code number
67 * @param string name format name from xml file
68 * @return int Moodle format code
70 public function trans_format($name) {
73 if ($name == 'moodle_auto_format') {
75 } else if ($name == 'html') {
77 } else if ($name == 'plain_text') {
79 } else if ($name == 'wiki_like') {
81 } else if ($name == 'markdown') {
82 return FORMAT_MARKDOWN;
84 debugging("Unrecognised text format '{$name}' in the import file. Assuming 'html'.");
90 * Translate human readable single answer option
91 * to internal code number
92 * @param string name true/false
93 * @return int internal code number
95 public function trans_single($name) {
97 if ($name == "false" || !$name) {
105 * process text string from xml file
106 * @param array $text bit of xml tree after ['text']
107 * @return string processed text.
109 public function import_text($text) {
110 // quick sanity check
114 $data = $text[0]['#'];
119 * return the value of a node, given a path to the node
120 * if it doesn't exist return the default value
121 * @param array xml data to read
122 * @param array path path to node expressed as array
123 * @param mixed default
124 * @param bool istext process as text
125 * @param string error if set value must exist, return false and issue message if not
126 * @return mixed value
128 public function getpath($xml, $path, $default, $istext=false, $error='') {
129 foreach ($path as $index) {
130 if (!isset($xml[$index])) {
131 if (!empty($error)) {
132 $this->error($error);
143 if (!is_string($xml)) {
144 $this->error(get_string('invalidxml', 'qformat_xml'));
154 * import parts of question common to all types
155 * @param $question array question question array from xml tree
156 * @return object question object
158 public function import_headers($question) {
161 // get some error strings
162 $error_noname = get_string('xmlimportnoname', 'qformat_xml');
163 $error_noquestion = get_string('xmlimportnoquestion', 'qformat_xml');
165 // this routine initialises the question object
166 $qo = $this->defaultquestion();
169 $qo->name = $this->getpath($question,
170 array('#', 'name', 0, '#', 'text', 0, '#'), '', true,
171 get_string('xmlimportnoname', 'qformat_xml'));
172 $qo->questiontext = $this->getpath($question,
173 array('#', 'questiontext', 0, '#', 'text', 0, '#'), '', true);
174 $qo->questiontextformat = $this->trans_format($this->getpath(
175 $question, array('#', 'questiontext', 0, '@', 'format'), 'html'));
177 $qo->questiontextfiles = $this->import_files($this->getpath($question,
178 array('#', 'questiontext', 0, '#', 'file'), array(), false));
180 // Backwards compatibility, deal with the old image tag.
181 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
182 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
183 if ($filedata && $filename) {
184 $data = new stdClass();
185 $data->content = $filedata;
186 $data->encoding = 'base64';
187 $data->name = $filename;
188 $qo->questiontextfiles[] = $data;
189 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
192 // restore files in generalfeedback
193 $qo->generalfeedback = $this->getpath($question,
194 array('#', 'generalfeedback', 0, '#', 'text', 0, '#'), $qo->generalfeedback, true);
195 $qo->generalfeedbackfiles = array();
196 $qo->generalfeedbackformat = $this->trans_format($this->getpath($question,
197 array('#', 'generalfeedback', 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
198 $qo->generalfeedbackfiles = $this->import_files($this->getpath($question,
199 array('#', 'generalfeedback', 0, '#', 'file'), array(), false));
201 $qo->defaultmark = $this->getpath($question,
202 array('#', 'defaultgrade', 0, '#'), $qo->defaultmark);
203 $qo->penalty = $this->getpath($question,
204 array('#', 'penalty', 0, '#'), $qo->penalty);
206 // Fix problematic rounding from old files:
207 if (abs($qo->penalty - 0.3333333) < 0.005) {
208 $qo->penalty = 0.3333333;
211 // Read the question tags.
212 if (!empty($CFG->usetags) && array_key_exists('tags', $question['#'])
213 && !empty($question['#']['tags'][0]['#']['tag'])) {
214 require_once($CFG->dirroot.'/tag/lib.php');
216 foreach ($question['#']['tags'][0]['#']['tag'] as $tagdata) {
217 $qo->tags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true);
225 * Import the common parts of a single answer
226 * @param array answer xml tree for single answer
227 * @param bool $withanswerfiles if true, the answers are HTML (or $defaultformat)
228 * and so may contain files, otherwise the answers are plain text.
229 * @param array Default text format for the feedback, and the answers if $withanswerfiles
231 * @return object answer object
233 public function import_answer($answer, $withanswerfiles = false, $defaultformat = 'html') {
234 $ans = new stdClass();
236 $ans->answer = array();
237 $ans->answer['text'] = $this->getpath($answer, array('#', 'text', 0, '#'), '', true);
238 $ans->answer['format'] = $this->trans_format($this->getpath($answer,
239 array('@', 'format'), $defaultformat));
240 if ($withanswerfiles) {
241 $ans->answer['files'] = $this->import_files($this->getpath($answer,
242 array('#', 'file'), array()));
244 $ans->answer['format'] = FORMAT_PLAIN;
247 $ans->feedback = array();
248 $ans->feedback['text'] = $this->getpath($answer,
249 array('#', 'feedback', 0, '#', 'text', 0, '#'), '', true);
250 $ans->feedback['format'] = $this->trans_format($this->getpath($answer,
251 array('#', 'feedback', 0, '@', 'format'), $defaultformat));
252 $ans->feedback['files'] = $this->import_files($this->getpath($answer,
253 array('#', 'feedback', 0, '#', 'file'), array()));
255 $ans->fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
261 * Import the common overall feedback fields.
262 * @param object $question the part of the XML relating to this question.
263 * @param object $qo the question data to add the fields to.
264 * @param bool $withshownumpartscorrect include the shownumcorrect field.
266 public function import_combined_feedback($qo, $questionxml, $withshownumpartscorrect = false) {
267 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
268 foreach ($fields as $field) {
270 $text['text'] = $this->getpath($questionxml,
271 array('#', $field, 0, '#', 'text', 0, '#'), '', true);
272 $text['format'] = $this->trans_format($this->getpath($questionxml,
273 array('#', $field, 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
274 $text['files'] = $this->import_files($this->getpath($questionxml,
275 array('#', $field, 0, '#', 'file'), array(), false));
280 if ($withshownumpartscorrect) {
281 $qo->shownumcorrect = array_key_exists('shownumcorrect', $questionxml['#']);
283 // Backwards compatibility:
284 if (array_key_exists('correctresponsesfeedback', $questionxml['#'])) {
285 $qo->shownumcorrect = $this->trans_single($this->getpath($questionxml,
286 array('#', 'correctresponsesfeedback', 0, '#'), 1));
292 * Import a question hint
293 * @param array $hintxml hint xml fragment.
294 * @param string $defaultformat the text format to assume for hints that do not specify.
295 * @return object hint for storing in the database.
297 public function import_hint($hintxml, $defaultformat) {
298 if (array_key_exists('hintcontent', $hintxml['#'])) {
299 // Backwards compatibility:
301 $hint = new stdClass();
302 $hint->hint = array('format' => FORMAT_HTML, 'files' => array());
303 $hint->hint['text'] = $this->getpath($hintxml,
304 array('#', 'hintcontent', 0, '#', 'text', 0, '#'), '', true);
305 $hint->hint['format'] = $this->trans_format($defaultformat);
306 $hint->hint['files'] = array();
307 $hint->shownumcorrect = $this->getpath($hintxml,
308 array('#', 'statenumberofcorrectresponses', 0, '#'), 0);
309 $hint->clearwrong = $this->getpath($hintxml,
310 array('#', 'clearincorrectresponses', 0, '#'), 0);
311 $hint->options = $this->getpath($hintxml,
312 array('#', 'showfeedbacktoresponses', 0, '#'), 0);
317 $hint = new stdClass();
318 $hint->hint['text'] = $this->getpath($hintxml,
319 array('#', 'text', 0, '#'), '', true);
320 $hint->hint['format'] = $this->trans_format($this->getpath($hintxml,
321 array('@', 'format'), $defaultformat));
322 $hint->hint['files'] = $this->import_files($this->getpath($hintxml,
323 array('#', 'file'), array(), false));
324 $hint->shownumcorrect = array_key_exists('shownumcorrect', $hintxml['#']);
325 $hint->clearwrong = array_key_exists('clearwrong', $hintxml['#']);
326 $hint->options = $this->getpath($hintxml, array('#', 'options', 0, '#'), '', true);
332 * Import all the question hints
334 * @param object $qo the question data that is being constructed.
335 * @param array $questionxml The xml representing the question.
336 * @param bool $withparts whether the extra fields relating to parts should be imported.
337 * @param bool $withoptions whether the extra options field should be imported.
338 * @param string $defaultformat the text format to assume for hints that do not specify.
339 * @return array of objects representing the hints in the file.
341 public function import_hints($qo, $questionxml, $withparts = false,
342 $withoptions = false, $defaultformat = 'html') {
343 if (!isset($questionxml['#']['hint'])) {
347 foreach ($questionxml['#']['hint'] as $hintxml) {
348 $hint = $this->import_hint($hintxml, $defaultformat);
349 $qo->hint[] = $hint->hint;
352 $qo->hintshownumcorrect[] = $hint->shownumcorrect;
353 $qo->hintclearwrong[] = $hint->clearwrong;
357 $qo->hintoptions[] = $hint->options;
363 * Import files from a node in the XML.
364 * @param array $xml an array of <file> nodes from the the parsed XML.
365 * @return array of things representing files - in the form that save_question expects.
367 public function import_files($xml) {
369 foreach ($xml as $file) {
370 $data = new stdClass();
371 $data->content = $file['#'];
372 $data->encoding = $file['@']['encoding'];
373 $data->name = $file['@']['name'];
380 * import multiple choice question
381 * @param array question question array from xml tree
382 * @return object question object
384 public function import_multichoice($question) {
386 $qo = $this->import_headers($question);
388 // 'header' parts particular to multichoice
389 $qo->qtype = MULTICHOICE;
390 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
391 $qo->single = $this->trans_single($single);
392 $shuffleanswers = $this->getpath($question,
393 array('#', 'shuffleanswers', 0, '#'), 'false');
394 $qo->answernumbering = $this->getpath($question,
395 array('#', 'answernumbering', 0, '#'), 'abc');
396 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
398 // There was a time on the 1.8 branch when it could output an empty
399 // answernumbering tag, so fix up any found.
400 if (empty($qo->answernumbering)) {
401 $qo->answernumbering = 'abc';
404 // Run through the answers
405 $answers = $question['#']['answer'];
407 foreach ($answers as $answer) {
408 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
409 $qo->answer[$acount] = $ans->answer;
410 $qo->fraction[$acount] = $ans->fraction;
411 $qo->feedback[$acount] = $ans->feedback;
415 $this->import_combined_feedback($qo, $question, true);
416 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
422 * Import cloze type question
423 * @param array question question array from xml tree
424 * @return object question object
426 public function import_multianswer($question) {
427 question_bank::get_qtype('multianswer');
429 $questiontext['text'] = $this->import_text($question['#']['questiontext'][0]['#']['text']);
430 $questiontext['format'] = FORMAT_HTML;
431 $questiontext['itemid'] = '';
432 $qo = qtype_multianswer_extract_question($questiontext);
434 // 'header' parts particular to multianswer
435 $qo->qtype = 'multianswer';
436 $qo->course = $this->course;
437 $qo->generalfeedback = '';
439 $qo->name = $this->import_text($question['#']['name'][0]['#']['text']);
440 $qo->questiontextformat = $questiontext['format'];
441 $qo->questiontext = $qo->questiontext['text'];
442 $qo->questiontextfiles = array();
444 // restore files in generalfeedback
445 $qo->generalfeedback = $this->getpath($question,
446 array('#', 'generalfeedback', 0, '#', 'text', 0, '#'), $qo->generalfeedback, true);
447 $qo->generalfeedbackformat = $this->trans_format($this->getpath($question,
448 array('#', 'generalfeedback', 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
449 $qo->generalfeedbackfiles = $this->import_files($this->getpath($question,
450 array('#', 'generalfeedback', 0, '#', 'file'), array(), false));
452 $qo->penalty = $this->getpath($question,
453 array('#', 'penalty', 0, '#'), $this->defaultquestion()->penalty);
454 // Fix problematic rounding from old files:
455 if (abs($qo->penalty - 0.3333333) < 0.005) {
456 $qo->penalty = 0.3333333;
459 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
465 * Import true/false type question
466 * @param array question question array from xml tree
467 * @return object question object
469 public function import_truefalse($question) {
472 $qo = $this->import_headers($question);
474 // 'header' parts particular to true/false
475 $qo->qtype = TRUEFALSE;
477 // In the past, it used to be assumed that the two answers were in the file
478 // true first, then false. Howevever that was not always true. Now, we
479 // try to match on the answer text, but in old exports, this will be a localised
480 // string, so if we don't find true or false, we fall back to the old system.
483 foreach ($question['#']['answer'] as $answer) {
484 $answertext = $this->getpath($answer,
485 array('#', 'text', 0, '#'), '', true);
486 $feedback = $this->getpath($answer,
487 array('#', 'feedback', 0, '#', 'text', 0, '#'), '', true);
488 $feedbackformat = $this->getpath($answer,
489 array('#', 'feedback', 0, '@', 'format'), $this->get_format($qo->questiontextformat));
490 $feedbackfiles = $this->getpath($answer,
491 array('#', 'feedback', 0, '#', 'file'), array());
493 foreach ($feedbackfiles as $file) {
494 $data = new stdClass();
495 $data->content = $file['#'];
496 $data->encoding = $file['@']['encoding'];
497 $data->name = $file['@']['name'];
500 if ($answertext != 'true' && $answertext != 'false') {
501 // Old style file, assume order is true/false.
504 $answertext = 'true';
506 $answertext = 'false';
510 if ($answertext == 'true') {
511 $qo->answer = ($answer['@']['fraction'] == 100);
512 $qo->correctanswer = $qo->answer;
513 $qo->feedbacktrue = array();
514 $qo->feedbacktrue['text'] = $feedback;
515 $qo->feedbacktrue['format'] = $this->trans_format($feedbackformat);
516 $qo->feedbacktrue['files'] = $files;
518 $qo->answer = ($answer['@']['fraction'] != 100);
519 $qo->correctanswer = $qo->answer;
520 $qo->feedbackfalse = array();
521 $qo->feedbackfalse['text'] = $feedback;
522 $qo->feedbackfalse['format'] = $this->trans_format($feedbackformat);
523 $qo->feedbackfalse['files'] = $files;
530 $a->questiontext = $qo->questiontext;
531 $a->answer = get_string($qo->correctanswer ? 'true' : 'false', 'qtype_truefalse');
532 echo $OUTPUT->notification(get_string('truefalseimporterror', 'qformat_xml', $a));
535 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
541 * Import short answer type question
542 * @param array question question array from xml tree
543 * @return object question object
545 public function import_shortanswer($question) {
547 $qo = $this->import_headers($question);
549 // header parts particular to shortanswer
550 $qo->qtype = SHORTANSWER;
553 $qo->usecase = $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase);
555 // Run through the answers
556 $answers = $question['#']['answer'];
558 foreach ($answers as $answer) {
559 $ans = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
560 $qo->answer[$acount] = $ans->answer['text'];
561 $qo->fraction[$acount] = $ans->fraction;
562 $qo->feedback[$acount] = $ans->feedback;
566 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
572 * Import description type question
573 * @param array question question array from xml tree
574 * @return object question object
576 public function import_description($question) {
578 $qo = $this->import_headers($question);
579 // header parts particular to shortanswer
580 $qo->qtype = DESCRIPTION;
581 $qo->defaultmark = 0;
587 * Import numerical type question
588 * @param array question question array from xml tree
589 * @return object question object
591 public function import_numerical($question) {
593 $qo = $this->import_headers($question);
595 // header parts particular to numerical
596 $qo->qtype = NUMERICAL;
599 $answers = $question['#']['answer'];
600 $qo->answer = array();
601 $qo->feedback = array();
602 $qo->fraction = array();
603 $qo->tolerance = array();
604 foreach ($answers as $answer) {
605 // answer outside of <text> is deprecated
606 $obj = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
607 $qo->answer[] = $obj->answer['text'];
608 if (empty($qo->answer)) {
611 $qo->feedback[] = $obj->feedback;
612 $qo->tolerance[] = $this->getpath($answer, array('#', 'tolerance', 0, '#'), 0);
614 // fraction as a tag is deprecated
615 $fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
616 $qo->fraction[] = $this->getpath($answer,
617 array('#', 'fraction', 0, '#'), $fraction); // deprecated
620 // Get the units array
622 $units = $this->getpath($question, array('#', 'units', 0, '#', 'unit'), array());
623 if (!empty($units)) {
624 $qo->multiplier = array();
625 foreach ($units as $unit) {
626 $qo->multiplier[] = $this->getpath($unit, array('#', 'multiplier', 0, '#'), 1);
627 $qo->unit[] = $this->getpath($unit, array('#', 'unit_name', 0, '#'), '', true);
630 $qo->unitgradingtype = $this->getpath($question, array('#', 'unitgradingtype', 0, '#'), 0);
631 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0);
632 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
633 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
634 $qo->instructions['text'] = '';
635 $qo->instructions['format'] = FORMAT_HTML;
636 $instructions = $this->getpath($question, array('#', 'instructions'), array());
637 if (!empty($instructions)) {
638 $qo->instructions = array();
639 $qo->instructions['text'] = $this->getpath($instructions,
640 array('0', '#', 'text', '0', '#'), '', true);
641 $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
642 array('0', '@', 'format'), $this->get_format($qo->questiontextformat)));
643 $qo->instructions['files'] = $this->import_files($this->getpath(
644 $instructions, array('0', '#', 'file'), array()));
647 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
653 * Import matching type question
654 * @param array question question array from xml tree
655 * @return object question object
657 public function import_match($question) {
659 $qo = $this->import_headers($question);
661 // header parts particular to matching
662 $qo->qtype = 'match';
663 $qo->shuffleanswers = $this->trans_single($this->getpath($question,
664 array('#', 'shuffleanswers', 0, '#'), 1));
666 // run through subquestions
667 $qo->subquestions = array();
668 $qo->subanswers = array();
669 foreach ($question['#']['subquestion'] as $subqxml) {
670 $subquestion = array();
671 $subquestion['text'] = $this->getpath($subqxml, array('#', 'text', 0, '#'), '', true);
672 $subquestion['format'] = $this->trans_format($this->getpath($subqxml,
673 array('@', 'format'), $this->get_format($qo->questiontextformat)));
674 $subquestion['files'] = $this->import_files($this->getpath($subqxml,
675 array('#', 'file'), array()));
677 $qo->subquestions[] = $subquestion;
678 $answers = $this->getpath($subqxml, array('#', 'answer'), array());
679 $qo->subanswers[] = $this->getpath($subqxml,
680 array('#', 'answer', 0, '#', 'text', 0, '#'), '', true);
683 $this->import_combined_feedback($qo, $question, true);
684 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
690 * Import essay type question
691 * @param array question question array from xml tree
692 * @return object question object
694 public function import_essay($question) {
696 $qo = $this->import_headers($question);
698 // header parts particular to essay
701 $qo->responseformat = $this->getpath($question,
702 array('#', 'responseformat', 0, '#'), 'editor');
703 $qo->responsefieldlines = $this->getpath($question,
704 array('#', 'responsefieldlines', 0, '#'), 15);
705 $qo->attachments = $this->getpath($question,
706 array('#', 'attachments', 0, '#'), 0);
707 $qo->graderinfo['text'] = $this->getpath($question,
708 array('#', 'graderinfo', 0, '#', 'text', 0, '#'), '', true);
709 $qo->graderinfo['format'] = $this->trans_format($this->getpath($question,
710 array('#', 'graderinfo', 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
711 $qo->graderinfo['files'] = $this->import_files($this->getpath($question,
712 array('#', 'graderinfo', '0', '#', 'file'), array()));
718 * Import a calculated question
719 * @param object $question the imported XML data.
721 public function import_calculated($question) {
724 $qo = $this->import_headers($question);
726 // header parts particular to calculated
727 $qo->qtype = CALCULATED;
728 $qo->synchronize = $this->getpath($question, array('#', 'synchronize', 0, '#'), 0);
729 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
730 $qo->single = $this->trans_single($single);
731 $shuffleanswers = $this->getpath($question, array('#', 'shuffleanswers', 0, '#'), 'false');
732 $qo->answernumbering = $this->getpath($question,
733 array('#', 'answernumbering', 0, '#'), 'abc');
734 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
736 $this->import_combined_feedback($qo, $question);
738 $qo->unitgradingtype = $this->getpath($question,
739 array('#', 'unitgradingtype', 0, '#'), 0);
740 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0);
741 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
742 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
743 $qo->instructions = $this->getpath($question,
744 array('#', 'instructions', 0, '#', 'text', 0, '#'), '', true);
745 if (!empty($instructions)) {
746 $qo->instructions = array();
747 $qo->instructions['text'] = $this->getpath($instructions,
748 array('0', '#', 'text', '0', '#'), '', true);
749 $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
750 array('0', '@', 'format'), $this->get_format($qo->questiontextformat)));
751 $qo->instructions['files'] = $this->import_files($this->getpath($instructions,
752 array('0', '#', 'file'), array()));
756 $answers = $question['#']['answer'];
757 $qo->answers = array();
758 $qo->feedback = array();
759 $qo->fraction = array();
760 $qo->tolerance = array();
761 $qo->tolerancetype = array();
762 $qo->correctanswerformat = array();
763 $qo->correctanswerlength = array();
764 $qo->feedback = array();
765 foreach ($answers as $answer) {
766 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
767 // answer outside of <text> is deprecated
768 if (empty($ans->answer['text'])) {
769 $ans->answer['text'] = '*';
771 $qo->answers[] = $ans->answer;
772 $qo->feedback[] = $ans->feedback;
773 $qo->tolerance[] = $answer['#']['tolerance'][0]['#'];
774 // fraction as a tag is deprecated
775 if (!empty($answer['#']['fraction'][0]['#'])) {
776 $qo->fraction[] = $answer['#']['fraction'][0]['#'];
778 $qo->fraction[] = $answer['@']['fraction'] / 100;
780 $qo->tolerancetype[] = $answer['#']['tolerancetype'][0]['#'];
781 $qo->correctanswerformat[] = $answer['#']['correctanswerformat'][0]['#'];
782 $qo->correctanswerlength[] = $answer['#']['correctanswerlength'][0]['#'];
786 if (isset($question['#']['units'][0]['#']['unit'])) {
787 $units = $question['#']['units'][0]['#']['unit'];
788 $qo->multiplier = array();
789 foreach ($units as $unit) {
790 $qo->multiplier[] = $unit['#']['multiplier'][0]['#'];
791 $qo->unit[] = $unit['#']['unit_name'][0]['#'];
794 $instructions = $this->getpath($question, array('#', 'instructions'), array());
795 if (!empty($instructions)) {
796 $qo->instructions = array();
797 $qo->instructions['text'] = $this->getpath($instructions,
798 array('0', '#', 'text', '0', '#'), '', true);
799 $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
800 array('0', '@', 'format'), $this->get_format($qo->questiontextformat)));
801 $qo->instructions['files'] = $this->import_files($this->getpath($instructions,
802 array('0', '#', 'file'), array()));
804 $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition'];
805 $qo->dataset = array();
806 $qo->datasetindex= 0;
807 foreach ($datasets as $dataset) {
809 $qo->dataset[$qo->datasetindex] = new stdClass();
810 $qo->dataset[$qo->datasetindex]->status =
811 $this->import_text($dataset['#']['status'][0]['#']['text']);
812 $qo->dataset[$qo->datasetindex]->name =
813 $this->import_text($dataset['#']['name'][0]['#']['text']);
814 $qo->dataset[$qo->datasetindex]->type =
815 $dataset['#']['type'][0]['#'];
816 $qo->dataset[$qo->datasetindex]->distribution =
817 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
818 $qo->dataset[$qo->datasetindex]->max =
819 $this->import_text($dataset['#']['maximum'][0]['#']['text']);
820 $qo->dataset[$qo->datasetindex]->min =
821 $this->import_text($dataset['#']['minimum'][0]['#']['text']);
822 $qo->dataset[$qo->datasetindex]->length =
823 $this->import_text($dataset['#']['decimals'][0]['#']['text']);
824 $qo->dataset[$qo->datasetindex]->distribution =
825 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
826 $qo->dataset[$qo->datasetindex]->itemcount = $dataset['#']['itemcount'][0]['#'];
827 $qo->dataset[$qo->datasetindex]->datasetitem = array();
828 $qo->dataset[$qo->datasetindex]->itemindex = 0;
829 $qo->dataset[$qo->datasetindex]->number_of_items =
830 $dataset['#']['number_of_items'][0]['#'];
831 $datasetitems = $dataset['#']['dataset_items'][0]['#']['dataset_item'];
832 foreach ($datasetitems as $datasetitem) {
833 $qo->dataset[$qo->datasetindex]->itemindex++;
834 $qo->dataset[$qo->datasetindex]->datasetitem[
835 $qo->dataset[$qo->datasetindex]->itemindex] = new stdClass();
836 $qo->dataset[$qo->datasetindex]->datasetitem[
837 $qo->dataset[$qo->datasetindex]->itemindex]->itemnumber =
838 $datasetitem['#']['number'][0]['#'];
839 $qo->dataset[$qo->datasetindex]->datasetitem[
840 $qo->dataset[$qo->datasetindex]->itemindex]->value =
841 $datasetitem['#']['value'][0]['#'];
845 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
851 * This is not a real question type. It's a dummy type used to specify the
852 * import category. The format is:
853 * <question type="category">
854 * <category>tom/dick/harry</category>
857 protected function import_category($question) {
858 $qo = new stdClass();
859 $qo->qtype = 'category';
860 $qo->category = $this->import_text($question['#']['category'][0]['#']['text']);
865 * Parse the array of lines into an array of questions
866 * this *could* burn memory - but it won't happen that much
867 * so fingers crossed!
868 * @param array of lines from the input file.
869 * @return array (of objects) question objects.
871 protected function readquestions($lines) {
872 // We just need it as one big string
873 $text = implode($lines, ' ');
876 // This converts xml to big nasty data structure
877 // the 0 means keep white space as it is (important for markdown format)
879 $xml = xmlize($text, 0, 'UTF-8', true);
880 } catch (xml_format_exception $e) {
881 $this->error($e->getMessage(), '');
884 // Set up array to hold all our questions
885 $questions = array();
887 // Iterate through questions
888 foreach ($xml['quiz']['#']['question'] as $question) {
889 $questiontype = $question['@']['type'];
891 if ($questiontype == 'multichoice') {
892 $qo = $this->import_multichoice($question);
893 } else if ($questiontype == 'truefalse') {
894 $qo = $this->import_truefalse($question);
895 } else if ($questiontype == 'shortanswer') {
896 $qo = $this->import_shortanswer($question);
897 } else if ($questiontype == 'numerical') {
898 $qo = $this->import_numerical($question);
899 } else if ($questiontype == 'description') {
900 $qo = $this->import_description($question);
901 } else if ($questiontype == 'matching' || $questiontype == 'match') {
902 $qo = $this->import_match($question);
903 } else if ($questiontype == 'cloze' || $questiontype == 'multianswer') {
904 $qo = $this->import_multianswer($question);
905 } else if ($questiontype == 'essay') {
906 $qo = $this->import_essay($question);
907 } else if ($questiontype == 'calculated') {
908 $qo = $this->import_calculated($question);
909 } else if ($questiontype == 'calculatedsimple') {
910 $qo = $this->import_calculated($question);
911 $qo->qtype = 'calculatedsimple';
912 } else if ($questiontype == 'calculatedmulti') {
913 $qo = $this->import_calculated($question);
914 $qo->qtype = 'calculatedmulti';
915 } else if ($questiontype == 'category') {
916 $qo = $this->import_category($question);
919 // Not a type we handle ourselves. See if the question type wants
921 if (!$qo = $this->try_importing_using_qtypes(
922 $question, null, null, $questiontype)) {
923 $this->error(get_string('xmltypeunsupported', 'qformat_xml', $questiontype));
928 // Stick the result in the $questions array
936 // EXPORT FUNCTIONS START HERE
938 public function export_file_extension() {
943 * Turn the internal question type name into a human readable form.
944 * (In the past, the code used to use integers internally. Now, it uses
945 * strings, so there is less need for this, but to maintain
946 * backwards-compatibility we change two of the type names.)
947 * @param string $qtype question type plugin name.
948 * @return string $qtype string to use in the file.
950 protected function get_qtype($qtype) {
962 * Convert internal Moodle text format code into
963 * human readable form
964 * @param int id internal code
965 * @return string format text
967 public function get_format($id) {
970 return 'moodle_auto_format';
977 case FORMAT_MARKDOWN:
985 * Convert internal single question code into
986 * human readable form
987 * @param int id single question code
988 * @return string single question string
990 public function get_single($id) {
1002 * Take a string, and wrap it in a CDATA secion, if that is required to make
1003 * the output XML valid.
1004 * @param string $string a string
1005 * @return string the string, wrapped in CDATA if necessary.
1007 public function xml_escape($string) {
1008 if (!empty($string) && htmlspecialchars($string) != $string) {
1009 return "<![CDATA[{$string}]]>";
1016 * Generates <text></text> tags, processing raw text therein
1017 * @param string $raw the content to output.
1018 * @param int $indent the current indent level.
1019 * @param bool $short stick it on one line.
1020 * @return string formatted text.
1022 public function writetext($raw, $indent = 0, $short = true) {
1023 $indent = str_repeat(' ', $indent);
1024 $raw = $this->xml_escape($raw);
1027 $xml = "$indent<text>$raw</text>\n";
1029 $xml = "$indent<text>\n$raw\n$indent</text>\n";
1035 protected function presave_process($content) {
1036 // Override to allow us to add xml headers and footers
1037 return '<?xml version="1.0" encoding="UTF-8"?>
1039 ' . $content . '</quiz>';
1043 * Turns question into an xml segment
1044 * @param object $question the question data.
1045 * @return string xml segment
1047 public function writequestion($question) {
1048 global $CFG, $OUTPUT;
1050 $fs = get_file_storage();
1051 $contextid = $question->contextid;
1052 // Get files used by the questiontext.
1053 $question->questiontextfiles = $fs->get_area_files(
1054 $contextid, 'question', 'questiontext', $question->id);
1055 // Get files used by the generalfeedback.
1056 $question->generalfeedbackfiles = $fs->get_area_files(
1057 $contextid, 'question', 'generalfeedback', $question->id);
1058 if (!empty($question->options->answers)) {
1059 foreach ($question->options->answers as $answer) {
1060 $answer->answerfiles = $fs->get_area_files(
1061 $contextid, 'question', 'answer', $answer->id);
1062 $answer->feedbackfiles = $fs->get_area_files(
1063 $contextid, 'question', 'answerfeedback', $answer->id);
1069 // Add a comment linking this to the original question id.
1070 $expout .= "<!-- question: $question->id -->\n";
1072 // Check question type
1073 $questiontype = $this->get_qtype($question->qtype);
1075 // Categories are a special case.
1076 if ($question->qtype == 'category') {
1077 $categorypath = $this->writetext($question->category);
1078 $expout .= " <question type=\"category\">\n";
1079 $expout .= " <category>\n";
1080 $expout .= " $categorypath\n";
1081 $expout .= " </category>\n";
1082 $expout .= " </question>\n";
1086 // Now we know we are are handing a real question.
1087 // Output the generic information.
1088 $expout .= " <question type=\"$questiontype\">\n";
1089 $expout .= " <name>\n";
1090 $expout .= $this->writetext($question->name, 3);
1091 $expout .= " </name>\n";
1092 $expout .= " <questiontext {$this->format($question->questiontextformat)}>\n";
1093 $expout .= $this->writetext($question->questiontext, 3);
1094 $expout .= $this->writefiles($question->questiontextfiles);
1095 $expout .= " </questiontext>\n";
1096 $expout .= " <generalfeedback {$this->format($question->generalfeedbackformat)}>\n";
1097 $expout .= $this->writetext($question->generalfeedback, 3);
1098 $expout .= $this->writefiles($question->generalfeedbackfiles);
1099 $expout .= " </generalfeedback>\n";
1100 if ($question->qtype != 'multianswer') {
1101 $expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
1103 $expout .= " <penalty>{$question->penalty}</penalty>\n";
1104 $expout .= " <hidden>{$question->hidden}</hidden>\n";
1106 // The rest of the output depends on question type.
1107 switch($question->qtype) {
1109 // not a qtype really - dummy used for category switching
1113 $trueanswer = $question->options->answers[$question->options->trueanswer];
1114 $trueanswer->answer = 'true';
1115 $expout .= $this->write_answer($trueanswer);
1117 $falseanswer = $question->options->answers[$question->options->falseanswer];
1118 $falseanswer->answer = 'false';
1119 $expout .= $this->write_answer($falseanswer);
1123 $expout .= " <single>" . $this->get_single($question->options->single) .
1125 $expout .= " <shuffleanswers>" .
1126 $this->get_single($question->options->shuffleanswers) .
1127 "</shuffleanswers>\n";
1128 $expout .= " <answernumbering>" . $question->options->answernumbering .
1129 "</answernumbering>\n";
1130 $expout .= $this->write_combined_feedback($question->options);
1131 $expout .= $this->write_answers($question->options->answers);
1135 $expout .= " <usecase>{$question->options->usecase}</usecase>\n";
1136 $expout .= $this->write_answers($question->options->answers);
1140 foreach ($question->options->answers as $answer) {
1141 $expout .= $this->write_answer($answer,
1142 " <tolerance>$answer->tolerance</tolerance>\n");
1145 $units = $question->options->units;
1146 if (count($units)) {
1147 $expout .= "<units>\n";
1148 foreach ($units as $unit) {
1149 $expout .= " <unit>\n";
1150 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1151 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1152 $expout .= " </unit>\n";
1154 $expout .= "</units>\n";
1156 if (isset($question->options->unitgradingtype)) {
1157 $expout .= " <unitgradingtype>" . $question->options->unitgradingtype .
1158 "</unitgradingtype>\n";
1160 if (isset($question->options->unitpenalty)) {
1161 $expout .= " <unitpenalty>{$question->options->unitpenalty}</unitpenalty>\n";
1163 if (isset($question->options->showunits)) {
1164 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1166 if (isset($question->options->unitsleft)) {
1167 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1169 if (!empty($question->options->instructionsformat)) {
1170 $files = $fs->get_area_files($contextid, 'qtype_numerical',
1171 'instruction', $question->id);
1172 $expout .= " <instructions " .
1173 $this->format($question->options->instructionsformat) . ">\n";
1174 $expout .= $this->writetext($question->options->instructions, 3);
1175 $expout .= $this->writefiles($files);
1176 $expout .= " </instructions>\n";
1181 $expout .= " <shuffleanswers>" .
1182 $this->get_single($question->options->shuffleanswers) .
1183 "</shuffleanswers>\n";
1184 $expout .= $this->write_combined_feedback($question->options);
1185 foreach ($question->options->subquestions as $subquestion) {
1186 $files = $fs->get_area_files($contextid, 'qtype_match',
1187 'subquestion', $subquestion->id);
1188 $expout .= " <subquestion " .
1189 $this->format($subquestion->questiontextformat) . ">\n";
1190 $expout .= $this->writetext($subquestion->questiontext, 3);
1191 $expout .= $this->writefiles($files);
1192 $expout .= " <answer>\n";
1193 $expout .= $this->writetext($subquestion->answertext, 4);
1194 $expout .= " </answer>\n";
1195 $expout .= " </subquestion>\n";
1200 // Nothing else to do.
1204 foreach ($question->options->questions as $index => $subq) {
1205 $expout = preg_replace('~{#' . $index . '}~', $subq->questiontext, $expout);
1210 $expout .= " <responseformat>" . $question->options->responseformat .
1211 "</responseformat>\n";
1212 $expout .= " <responsefieldlines>" . $question->options->responsefieldlines .
1213 "</responsefieldlines>\n";
1214 $expout .= " <attachments>" . $question->options->attachments .
1216 $expout .= " <graderinfo " .
1217 $this->format($question->options->graderinfoformat) . ">\n";
1218 $expout .= $this->writetext($question->options->graderinfo, 3);
1219 $expout .= $this->writefiles($fs->get_area_files($contextid, 'qtype_essay',
1220 'graderinfo', $question->id));
1221 $expout .= " </graderinfo>\n";
1225 case 'calculatedsimple':
1226 case 'calculatedmulti':
1227 $expout .= " <synchronize>{$question->options->synchronize}</synchronize>\n";
1228 $expout .= " <single>{$question->options->single}</single>\n";
1229 $expout .= " <answernumbering>" . $question->options->answernumbering .
1230 "</answernumbering>\n";
1231 $expout .= " <shuffleanswers>" . $question->options->shuffleanswers .
1232 "</shuffleanswers>\n";
1234 $component = 'qtype_' . $question->qtype;
1235 $files = $fs->get_area_files($contextid, $component,
1236 'correctfeedback', $question->id);
1237 $expout .= " <correctfeedback>\n";
1238 $expout .= $this->writetext($question->options->correctfeedback, 3);
1239 $expout .= $this->writefiles($files);
1240 $expout .= " </correctfeedback>\n";
1242 $files = $fs->get_area_files($contextid, $component,
1243 'partiallycorrectfeedback', $question->id);
1244 $expout .= " <partiallycorrectfeedback>\n";
1245 $expout .= $this->writetext($question->options->partiallycorrectfeedback, 3);
1246 $expout .= $this->writefiles($files);
1247 $expout .= " </partiallycorrectfeedback>\n";
1249 $files = $fs->get_area_files($contextid, $component,
1250 'incorrectfeedback', $question->id);
1251 $expout .= " <incorrectfeedback>\n";
1252 $expout .= $this->writetext($question->options->incorrectfeedback, 3);
1253 $expout .= $this->writefiles($files);
1254 $expout .= " </incorrectfeedback>\n";
1256 foreach ($question->options->answers as $answer) {
1257 $percent = 100 * $answer->fraction;
1258 $expout .= "<answer fraction=\"$percent\">\n";
1259 // "<text/>" tags are an added feature, old files won't have them
1260 $expout .= " <text>{$answer->answer}</text>\n";
1261 $expout .= " <tolerance>{$answer->tolerance}</tolerance>\n";
1262 $expout .= " <tolerancetype>{$answer->tolerancetype}</tolerancetype>\n";
1263 $expout .= " <correctanswerformat>" .
1264 $answer->correctanswerformat . "</correctanswerformat>\n";
1265 $expout .= " <correctanswerlength>" .
1266 $answer->correctanswerlength . "</correctanswerlength>\n";
1267 $expout .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1268 $files = $fs->get_area_files($contextid, $component,
1269 'instruction', $question->id);
1270 $expout .= $this->writetext($answer->feedback);
1271 $expout .= $this->writefiles($answer->feedbackfiles);
1272 $expout .= " </feedback>\n";
1273 $expout .= "</answer>\n";
1275 if (isset($question->options->unitgradingtype)) {
1276 $expout .= " <unitgradingtype>" .
1277 $question->options->unitgradingtype . "</unitgradingtype>\n";
1279 if (isset($question->options->unitpenalty)) {
1280 $expout .= " <unitpenalty>" .
1281 $question->options->unitpenalty . "</unitpenalty>\n";
1283 if (isset($question->options->showunits)) {
1284 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1286 if (isset($question->options->unitsleft)) {
1287 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1290 if (isset($question->options->instructionsformat)) {
1291 $files = $fs->get_area_files($contextid, $component,
1292 'instruction', $question->id);
1293 $expout .= " <instructions " .
1294 $this->format($question->options->instructionsformat) . ">\n";
1295 $expout .= $this->writetext($question->options->instructions, 3);
1296 $expout .= $this->writefiles($files);
1297 $expout .= " </instructions>\n";
1300 if (isset($question->options->units)) {
1301 $units = $question->options->units;
1302 if (count($units)) {
1303 $expout .= "<units>\n";
1304 foreach ($units as $unit) {
1305 $expout .= " <unit>\n";
1306 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1307 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1308 $expout .= " </unit>\n";
1310 $expout .= "</units>\n";
1314 // The tag $question->export_process has been set so we get all the
1315 // data items in the database from the function
1316 // qtype_calculated::get_question_options calculatedsimple defaults
1318 if (isset($question->options->datasets) && count($question->options->datasets)) {
1319 $expout .= "<dataset_definitions>\n";
1320 foreach ($question->options->datasets as $def) {
1321 $expout .= "<dataset_definition>\n";
1322 $expout .= " <status>".$this->writetext($def->status)."</status>\n";
1323 $expout .= " <name>".$this->writetext($def->name)."</name>\n";
1324 if ($question->qtype == CALCULATED) {
1325 $expout .= " <type>calculated</type>\n";
1327 $expout .= " <type>calculatedsimple</type>\n";
1329 $expout .= " <distribution>" . $this->writetext($def->distribution) .
1330 "</distribution>\n";
1331 $expout .= " <minimum>" . $this->writetext($def->minimum) .
1333 $expout .= " <maximum>" . $this->writetext($def->maximum) .
1335 $expout .= " <decimals>" . $this->writetext($def->decimals) .
1337 $expout .= " <itemcount>$def->itemcount</itemcount>\n";
1338 if ($def->itemcount > 0) {
1339 $expout .= " <dataset_items>\n";
1340 foreach ($def->items as $item) {
1341 $expout .= " <dataset_item>\n";
1342 $expout .= " <number>".$item->itemnumber."</number>\n";
1343 $expout .= " <value>".$item->value."</value>\n";
1344 $expout .= " </dataset_item>\n";
1346 $expout .= " </dataset_items>\n";
1347 $expout .= " <number_of_items>" . $def->number_of_items .
1348 "</number_of_items>\n";
1350 $expout .= "</dataset_definition>\n";
1352 $expout .= "</dataset_definitions>\n";
1357 // try support by optional plugin
1358 if (!$data = $this->try_exporting_using_qtypes($question->qtype, $question)) {
1359 notify(get_string('unsupportedexport', 'qformat_xml', $question->qtype));
1364 // Output any hints.
1365 $expout .= $this->write_hints($question);
1367 // Write the question tags.
1368 if (!empty($CFG->usetags)) {
1369 require_once($CFG->dirroot.'/tag/lib.php');
1370 $tags = tag_get_tags_array('question', $question->id);
1371 if (!empty($tags)) {
1372 $expout .= " <tags>\n";
1373 foreach ($tags as $tag) {
1374 $expout .= " <tag>" . $this->writetext($tag, 0, true) . "</tag>\n";
1376 $expout .= " </tags>\n";
1380 // close the question tag
1381 $expout .= " </question>\n";
1386 public function write_answers($answers) {
1387 if (empty($answers)) {
1391 foreach ($answers as $answer) {
1392 $output .= $this->write_answer($answer);
1397 public function write_answer($answer, $extra = '') {
1398 $percent = $answer->fraction * 100;
1400 $output .= " <answer fraction=\"$percent\" {$this->format($answer->answerformat)}>\n";
1401 $output .= $this->writetext($answer->answer, 3);
1402 $output .= $this->writefiles($answer->answerfiles);
1403 $output .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1404 $output .= $this->writetext($answer->feedback, 4);
1405 $output .= $this->writefiles($answer->feedbackfiles);
1406 $output .= " </feedback>\n";
1408 $output .= " </answer>\n";
1412 public function write_hints($question) {
1413 if (empty($question->hints)) {
1418 foreach ($question->hints as $hint) {
1419 $output .= $this->write_hint($hint);
1425 * @param unknown_type $format a FORMAT_... constant.
1426 * @return string the attribute to add to an XML tag.
1428 protected function format($format) {
1429 return 'format="' . $this->get_format($format) . '"';
1432 public function write_hint($hint) {
1434 $output .= " <hint {$this->format($hint->hintformat)}>\n";
1435 $output .= ' ' . $this->writetext($hint->hint);
1436 if (!empty($hint->shownumcorrect)) {
1437 $output .= " <shownumcorrect/>\n";
1439 if (!empty($hint->clearwrong)) {
1440 $output .= " <clearwrong/>\n";
1442 if (!empty($hint->options)) {
1443 $output .= ' <options>' . $this->xml_escape($hint->options) . "</options>\n";
1445 $output .= " </hint>\n";
1449 public function write_combined_feedback($questionoptions) {
1450 $output = " <correctfeedback {$this->format($questionoptions->correctfeedbackformat)}>
1451 {$this->writetext($questionoptions->correctfeedback)} </correctfeedback>
1452 <partiallycorrectfeedback {$this->format($questionoptions->partiallycorrectfeedbackformat)}>
1453 {$this->writetext($questionoptions->partiallycorrectfeedback)} </partiallycorrectfeedback>
1454 <incorrectfeedback {$this->format($questionoptions->incorrectfeedbackformat)}>
1455 {$this->writetext($questionoptions->incorrectfeedback)} </incorrectfeedback>\n";
1456 if (!empty($questionoptions->shownumcorrect)) {
1457 $output .= " <shownumcorrect/>\n";