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 protected 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 return 0; // or maybe warning required
89 * Translate human readable single answer option
90 * to internal code number
91 * @param string name true/false
92 * @return int internal code number
94 public function trans_single($name) {
96 if ($name == "false" || !$name) {
104 * process text string from xml file
105 * @param array $text bit of xml tree after ['text']
106 * @return string processed text.
108 public function import_text($text) {
109 // quick sanity check
113 $data = $text[0]['#'];
118 * return the value of a node, given a path to the node
119 * if it doesn't exist return the default value
120 * @param array xml data to read
121 * @param array path path to node expressed as array
122 * @param mixed default
123 * @param bool istext process as text
124 * @param string error if set value must exist, return false and issue message if not
125 * @return mixed value
127 public function getpath($xml, $path, $default, $istext=false, $error='') {
128 foreach ($path as $index) {
129 if (!isset($xml[$index])) {
130 if (!empty($error)) {
131 $this->error($error);
142 if (!is_string($xml)) {
143 $this->error(get_string('invalidxml', 'qformat_xml'));
153 * import parts of question common to all types
154 * @param $question array question question array from xml tree
155 * @return object question object
157 public function import_headers($question) {
160 // get some error strings
161 $error_noname = get_string('xmlimportnoname', 'qformat_xml');
162 $error_noquestion = get_string('xmlimportnoquestion', 'qformat_xml');
164 // this routine initialises the question object
165 $qo = $this->defaultquestion();
168 $qo->name = $this->getpath($question,
169 array('#', 'name', 0, '#', 'text', 0, '#'), '', true,
170 get_string('xmlimportnoname', 'qformat_xml'));
171 $qo->questiontext = $this->getpath($question,
172 array('#', 'questiontext', 0, '#', 'text', 0, '#'), '', true);
173 $qo->questiontextformat = $this->trans_format($this->getpath(
174 $question, array('#', 'questiontext', 0, '@', 'format'), ''));
176 $qo->questiontextfiles = $this->import_files($this->getpath($question,
177 array('#', 'questiontext', 0, '#', 'file'), array(), false));
179 // Backwards compatibility, deal with the old image tag.
180 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
181 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
182 if ($filedata && $filename) {
183 $data = new stdClass();
184 $data->content = $filedata;
185 $data->encoding = 'base64';
186 $data->name = $filename;
187 $qo->questiontextfiles[] = $data;
188 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
191 // restore files in generalfeedback
192 $qo->generalfeedback = $this->getpath($question,
193 array('#', 'generalfeedback', 0, '#', 'text', 0, '#'), $qo->generalfeedback, true);
194 $qo->generalfeedbackfiles = array();
195 $qo->generalfeedbackformat = $this->trans_format($this->getpath($question,
196 array('#', 'generalfeedback', 0, '@', 'format'), 'moodle_auto_format'));
197 $qo->generalfeedbackfiles = $this->import_files($this->getpath($question,
198 array('#', 'generalfeedback', 0, '#', 'file'), array(), false));
200 $qo->defaultmark = $this->getpath($question,
201 array('#', 'defaultgrade', 0, '#'), $qo->defaultmark);
202 $qo->penalty = $this->getpath($question,
203 array('#', 'penalty', 0, '#'), $qo->penalty);
205 // Fix problematic rounding from old files:
206 if (abs($qo->penalty - 0.3333333) < 0.005) {
207 $qo->penalty = 0.3333333;
210 // Read the question tags.
211 if (!empty($CFG->usetags) && array_key_exists('tags', $question['#'])
212 && !empty($question['#']['tags'][0]['#']['tag'])) {
213 require_once($CFG->dirroot.'/tag/lib.php');
215 foreach ($question['#']['tags'][0]['#']['tag'] as $tagdata) {
216 $qo->tags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true);
224 * Import the common parts of a single answer
225 * @param array answer xml tree for single answer
226 * @return object answer object
228 public function import_answer($answer, $withanswerfiles = false) {
229 $ans = new stdClass();
231 $ans->answer = array();
232 $ans->answer['text'] = $this->getpath($answer, array('#', 'text', 0, '#'), '', true);
233 $ans->answer['format'] = $this->trans_format($this->getpath($answer,
234 array('@', 'format'), 'moodle_auto_format'));
235 if ($withanswerfiles) {
236 $ans->answer['files'] = $this->import_files($this->getpath($answer,
237 array('#', 'file'), array()));
240 $ans->feedback = array();
241 $ans->feedback['text'] = $this->getpath($answer,
242 array('#', 'feedback', 0, '#', 'text', 0, '#'), '', true);
243 $ans->feedback['format'] = $this->trans_format($this->getpath($answer,
244 array('#', 'feedback', 0, '@', 'format'), 'moodle_auto_format'));
245 $ans->feedback['files'] = $this->import_files($this->getpath($answer,
246 array('#', 'feedback', 0, '#', 'file'), array()));
248 $ans->fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
254 * Import the common overall feedback fields.
255 * @param object $question the part of the XML relating to this question.
256 * @param object $qo the question data to add the fields to.
257 * @param bool $withshownumpartscorrect include the shownumcorrect field.
259 public function import_combined_feedback($qo, $questionxml, $withshownumpartscorrect = false) {
260 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
261 foreach ($fields as $field) {
263 $text['text'] = $this->getpath($questionxml,
264 array('#', $field, 0, '#', 'text', 0, '#'), '', true);
265 $text['format'] = $this->trans_format($this->getpath($questionxml,
266 array('#', $field, 0, '@', 'format'), 'moodle_auto_format'));
267 $text['files'] = $this->import_files($this->getpath($questionxml,
268 array('#', $field, 0, '#', 'file'), array(), false));
273 if ($withshownumpartscorrect) {
274 $qo->shownumcorrect = array_key_exists('shownumcorrect', $questionxml['#']);
276 // Backwards compatibility:
277 if (array_key_exists('correctresponsesfeedback', $questionxml['#'])) {
278 $qo->shownumcorrect = $this->trans_single($this->getpath($questionxml,
279 array('#', 'correctresponsesfeedback', 0, '#'), 1));
285 * Import a question hint
286 * @param array $hintxml hint xml fragment.
287 * @return object hint for storing in the database.
289 public function import_hint($hintxml) {
290 if (array_key_exists('hintcontent', $hintxml['#'])) {
291 // Backwards compatibility:
293 $hint = new stdClass();
294 $hint->hint = array('format' => FORMAT_HTML, 'files' => array());
295 $hint->hint['text'] = $this->getpath($hintxml,
296 array('#', 'hintcontent', 0, '#', 'text', 0, '#'), '', true);
297 $hint->shownumcorrect = $this->getpath($hintxml,
298 array('#', 'statenumberofcorrectresponses', 0, '#'), 0);
299 $hint->clearwrong = $this->getpath($hintxml,
300 array('#', 'clearincorrectresponses', 0, '#'), 0);
301 $hint->options = $this->getpath($hintxml,
302 array('#', 'showfeedbacktoresponses', 0, '#'), 0);
307 $hint = new stdClass();
308 $hint->hint['text'] = $this->getpath($hintxml,
309 array('#', 'text', 0, '#'), '', true);
310 $hint->hint['format'] = $this->trans_format($this->getpath($hintxml,
311 array('@', 'format'), 'moodle_auto_format'));
312 $hint->hint['files'] = $this->import_files($this->getpath($hintxml,
313 array('#', 'file'), array(), false));
314 $hint->shownumcorrect = array_key_exists('shownumcorrect', $hintxml['#']);
315 $hint->clearwrong = array_key_exists('clearwrong', $hintxml['#']);
316 $hint->options = $this->getpath($hintxml, array('#', 'options', 0, '#'), '', true);
322 * Import all the question hints
324 * @param object $qo the question data that is being constructed.
325 * @param array $hintsxml hints xml fragment.
327 public function import_hints($qo, $questionxml, $withparts = false, $withoptions = false) {
328 if (!isset($questionxml['#']['hint'])) {
332 foreach ($questionxml['#']['hint'] as $hintxml) {
333 $hint = $this->import_hint($hintxml);
334 $qo->hint[] = $hint->hint;
337 $qo->hintshownumcorrect[] = $hint->shownumcorrect;
338 $qo->hintclearwrong[] = $hint->clearwrong;
342 $qo->hintoptions[] = $hint->options;
348 * Import files from a node in the XML.
349 * @param array $xml an array of <file> nodes from the the parsed XML.
350 * @return array of things representing files - in the form that save_question expects.
352 public function import_files($xml) {
354 foreach ($xml as $file) {
355 $data = new stdClass();
356 $data->content = $file['#'];
357 $data->encoding = $file['@']['encoding'];
358 $data->name = $file['@']['name'];
365 * import multiple choice question
366 * @param array question question array from xml tree
367 * @return object question object
369 public function import_multichoice($question) {
371 $qo = $this->import_headers($question);
373 // 'header' parts particular to multichoice
374 $qo->qtype = MULTICHOICE;
375 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
376 $qo->single = $this->trans_single($single);
377 $shuffleanswers = $this->getpath($question,
378 array('#', 'shuffleanswers', 0, '#'), 'false');
379 $qo->answernumbering = $this->getpath($question,
380 array('#', 'answernumbering', 0, '#'), 'abc');
381 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
383 // There was a time on the 1.8 branch when it could output an empty
384 // answernumbering tag, so fix up any found.
385 if (empty($qo->answernumbering)) {
386 $qo->answernumbering = 'abc';
389 // Run through the answers
390 $answers = $question['#']['answer'];
392 foreach ($answers as $answer) {
393 $ans = $this->import_answer($answer, true);
394 $qo->answer[$acount] = $ans->answer;
395 $qo->fraction[$acount] = $ans->fraction;
396 $qo->feedback[$acount] = $ans->feedback;
400 $this->import_combined_feedback($qo, $question, true);
401 $this->import_hints($qo, $question, true);
407 * Import cloze type question
408 * @param array question question array from xml tree
409 * @return object question object
411 public function import_multianswer($question) {
412 question_bank::get_qtype('multianswer');
414 $questiontext['text'] = $this->import_text($question['#']['questiontext'][0]['#']['text']);
415 $questiontext['format'] = '1';
416 $questiontext['itemid'] = '';
417 $qo = qtype_multianswer_extract_question($questiontext);
419 // 'header' parts particular to multianswer
420 $qo->qtype = 'multianswer';
421 $qo->course = $this->course;
422 $qo->generalfeedback = '';
423 // restore files in generalfeedback
424 $qo->generalfeedback = $this->getpath($question,
425 array('#', 'generalfeedback', 0, '#', 'text', 0, '#'), $qo->generalfeedback, true);
426 $qo->generalfeedbackformat = $this->trans_format($this->getpath($question,
427 array('#', 'generalfeedback', 0, '@', 'format'), 'moodle_auto_format'));
428 $qo->generalfeedbackfiles = $this->import_files($this->getpath($question,
429 array('#', 'generalfeedback', 0, '#', 'file'), array(), false));
431 $qo->name = $this->import_text($question['#']['name'][0]['#']['text']);
432 $qo->questiontext = $qo->questiontext['text'];
433 $qo->questiontextformat = '';
435 $qo->penalty = $this->getpath($question,
436 array('#', 'penalty', 0, '#'), $this->defaultquestion()->penalty);
437 // Fix problematic rounding from old files:
438 if (abs($qo->penalty - 0.3333333) < 0.005) {
439 $qo->penalty = 0.3333333;
442 $this->import_hints($qo, $question);
448 * Import true/false type question
449 * @param array question question array from xml tree
450 * @return object question object
452 public function import_truefalse($question) {
455 $qo = $this->import_headers($question);
457 // 'header' parts particular to true/false
458 $qo->qtype = TRUEFALSE;
460 // In the past, it used to be assumed that the two answers were in the file
461 // true first, then false. Howevever that was not always true. Now, we
462 // try to match on the answer text, but in old exports, this will be a localised
463 // string, so if we don't find true or false, we fall back to the old system.
466 foreach ($question['#']['answer'] as $answer) {
467 $answertext = $this->getpath($answer,
468 array('#', 'text', 0, '#'), '', true);
469 $feedback = $this->getpath($answer,
470 array('#', 'feedback', 0, '#', 'text', 0, '#'), '', true);
471 $feedbackformat = $this->getpath($answer,
472 array('#', 'feedback', 0, '@', 'format'), 'moodle_auto_format');
473 $feedbackfiles = $this->getpath($answer,
474 array('#', 'feedback', 0, '#', 'file'), array());
476 foreach ($feedbackfiles as $file) {
477 $data = new stdClass();
478 $data->content = $file['#'];
479 $data->encoding = $file['@']['encoding'];
480 $data->name = $file['@']['name'];
483 if ($answertext != 'true' && $answertext != 'false') {
484 // Old style file, assume order is true/false.
487 $answertext = 'true';
489 $answertext = 'false';
493 if ($answertext == 'true') {
494 $qo->answer = ($answer['@']['fraction'] == 100);
495 $qo->correctanswer = $qo->answer;
496 $qo->feedbacktrue = array();
497 $qo->feedbacktrue['text'] = $feedback;
498 $qo->feedbacktrue['format'] = $this->trans_format($feedbackformat);
499 $qo->feedbacktrue['files'] = $files;
501 $qo->answer = ($answer['@']['fraction'] != 100);
502 $qo->correctanswer = $qo->answer;
503 $qo->feedbackfalse = array();
504 $qo->feedbackfalse['text'] = $feedback;
505 $qo->feedbackfalse['format'] = $this->trans_format($feedbackformat);
506 $qo->feedbackfalse['files'] = $files;
513 $a->questiontext = $qo->questiontext;
514 $a->answer = get_string($qo->correctanswer ? 'true' : 'false', 'qtype_truefalse');
515 echo $OUTPUT->notification(get_string('truefalseimporterror', 'qformat_xml', $a));
518 $this->import_hints($qo, $question);
524 * Import short answer type question
525 * @param array question question array from xml tree
526 * @return object question object
528 public function import_shortanswer($question) {
530 $qo = $this->import_headers($question);
532 // header parts particular to shortanswer
533 $qo->qtype = SHORTANSWER;
536 $qo->usecase = $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase);
538 // Run through the answers
539 $answers = $question['#']['answer'];
541 foreach ($answers as $answer) {
542 $ans = $this->import_answer($answer);
543 $qo->answer[$acount] = $ans->answer['text'];
544 $qo->fraction[$acount] = $ans->fraction;
545 $qo->feedback[$acount] = $ans->feedback;
549 $this->import_hints($qo, $question);
555 * Import description type question
556 * @param array question question array from xml tree
557 * @return object question object
559 public function import_description($question) {
561 $qo = $this->import_headers($question);
562 // header parts particular to shortanswer
563 $qo->qtype = DESCRIPTION;
564 $qo->defaultmark = 0;
570 * Import numerical type question
571 * @param array question question array from xml tree
572 * @return object question object
574 public function import_numerical($question) {
576 $qo = $this->import_headers($question);
578 // header parts particular to numerical
579 $qo->qtype = NUMERICAL;
582 $answers = $question['#']['answer'];
583 $qo->answer = array();
584 $qo->feedback = array();
585 $qo->fraction = array();
586 $qo->tolerance = array();
587 foreach ($answers as $answer) {
588 // answer outside of <text> is deprecated
589 $obj = $this->import_answer($answer);
590 $qo->answer[] = $obj->answer['text'];
591 if (empty($qo->answer)) {
594 $qo->feedback[] = $obj->feedback;
595 $qo->tolerance[] = $this->getpath($answer, array('#', 'tolerance', 0, '#'), 0);
597 // fraction as a tag is deprecated
598 $fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
599 $qo->fraction[] = $this->getpath($answer,
600 array('#', 'fraction', 0, '#'), $fraction); // deprecated
603 // Get the units array
605 $units = $this->getpath($question, array('#', 'units', 0, '#', 'unit'), array());
606 if (!empty($units)) {
607 $qo->multiplier = array();
608 foreach ($units as $unit) {
609 $qo->multiplier[] = $this->getpath($unit, array('#', 'multiplier', 0, '#'), 1);
610 $qo->unit[] = $this->getpath($unit, array('#', 'unit_name', 0, '#'), '', true);
613 $qo->unitgradingtype = $this->getpath($question, array('#', 'unitgradingtype', 0, '#'), 0);
614 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0);
615 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
616 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
617 $qo->instructions['text'] = '';
618 $qo->instructions['format'] = FORMAT_HTML;
619 $instructions = $this->getpath($question, array('#', 'instructions'), array());
620 if (!empty($instructions)) {
621 $qo->instructions = array();
622 $qo->instructions['text'] = $this->getpath($instructions,
623 array('0', '#', 'text', '0', '#'), '', true);
624 $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
625 array('0', '@', 'format'), 'moodle_auto_format'));
626 $qo->instructions['files'] = $this->import_files($this->getpath(
627 $instructions, array('0', '#', 'file'), array()));
630 $this->import_hints($qo, $question);
636 * Import matching type question
637 * @param array question question array from xml tree
638 * @return object question object
640 public function import_match($question) {
642 $qo = $this->import_headers($question);
644 // header parts particular to matching
645 $qo->qtype = 'match';
646 $qo->shuffleanswers = $this->trans_single($this->getpath($question,
647 array('#', 'shuffleanswers', 0, '#'), 1));
649 // run through subquestions
650 $qo->subquestions = array();
651 $qo->subanswers = array();
652 foreach ($question['#']['subquestion'] as $subqxml) {
653 $subquestion = array();
654 $subquestion['text'] = $this->getpath($subqxml, array('#', 'text', 0, '#'), '', true);
655 $subquestion['format'] = $this->trans_format($this->getpath($subqxml,
656 array('@', 'format'), 'moodle_auto_format'));
657 $subquestion['files'] = $this->import_files($this->getpath($subqxml,
658 array('#', 'file'), array()));
660 $qo->subquestions[] = $subquestion;
661 $answers = $this->getpath($subqxml, array('#', 'answer'), array());
662 $qo->subanswers[] = $this->getpath($subqxml,
663 array('#', 'answer', 0, '#', 'text', 0, '#'), '', true);
666 $this->import_combined_feedback($qo, $question, true);
667 $this->import_hints($qo, $question, true);
673 * Import essay type question
674 * @param array question question array from xml tree
675 * @return object question object
677 public function import_essay($question) {
679 $qo = $this->import_headers($question);
681 // header parts particular to essay
684 $qo->responseformat = $this->getpath($question,
685 array('#', 'responseformat', 0, '#'), 'editor');
686 $qo->responsefieldlines = $this->getpath($question,
687 array('#', 'responsefieldlines', 0, '#'), 15);
688 $qo->attachments = $this->getpath($question,
689 array('#', 'attachments', 0, '#'), 0);
690 $qo->graderinfo['text'] = $this->getpath($question,
691 array('#', 'graderinfo', 0, '#', 'text', 0, '#'), '', true);
692 $qo->graderinfo['format'] = $this->trans_format($this->getpath($question,
693 array('#', 'graderinfo', 0, '@', 'format'), 'moodle_auto_format'));
694 $qo->graderinfo['files'] = $this->import_files($this->getpath($question,
695 array('#', 'graderinfo', '0', '#', 'file'), array()));
701 * Import a calculated question
702 * @param object $question the imported XML data.
704 public function import_calculated($question) {
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, '@', 'format'), '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()));
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, true);
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]['#'];
784 $qo->fraction[] = $answer['@']['fraction'] / 100;
786 $qo->tolerancetype[] = $answer['#']['tolerancetype'][0]['#'];
787 $qo->correctanswerformat[] = $answer['#']['correctanswerformat'][0]['#'];
788 $qo->correctanswerlength[] = $answer['#']['correctanswerlength'][0]['#'];
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) {
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);
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>
863 protected function import_category($question) {
864 $qo = new stdClass();
865 $qo->qtype = 'category';
866 $qo->category = $this->import_text($question['#']['category'][0]['#']['text']);
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, ' ');
882 // This converts xml to big nasty data structure
883 // the 0 means keep white space as it is (important for markdown format)
885 $xml = xmlize($text, 0, 'UTF-8', true);
886 } catch (xml_format_exception $e) {
887 $this->error($e->getMessage(), '');
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' || $questiontype == 'match') {
908 $qo = $this->import_match($question);
909 } else if ($questiontype == 'cloze' || $questiontype == 'multianswer') {
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 == 'calculatedsimple') {
916 $qo = $this->import_calculated($question);
917 $qo->qtype = 'calculatedsimple';
918 } else if ($questiontype == 'calculatedmulti') {
919 $qo = $this->import_calculated($question);
920 $qo->qtype = 'calculatedmulti';
921 } else if ($questiontype == 'category') {
922 $qo = $this->import_category($question);
925 // Not a type we handle ourselves. See if the question type wants
927 if (!$qo = $this->try_importing_using_qtypes(
928 $question, null, null, $questiontype)) {
929 $this->error(get_string('xmltypeunsupported', 'qformat_xml', $questiontype));
934 // Stick the result in the $questions array
942 // EXPORT FUNCTIONS START HERE
944 public function export_file_extension() {
949 * Turn the internal question type name into a human readable form.
950 * (In the past, the code used to use integers internally. Now, it uses
951 * strings, so there is less need for this, but to maintain
952 * backwards-compatibility we change two of the type names.)
953 * @param string $qtype question type plugin name.
954 * @return string $qtype string to use in the file.
956 protected function get_qtype($qtype) {
968 * Convert internal Moodle text format code into
969 * human readable form
970 * @param int id internal code
971 * @return string format text
973 protected function get_format($id) {
976 return 'moodle_auto_format';
983 case FORMAT_MARKDOWN:
991 * Convert internal single question code into
992 * human readable form
993 * @param int id single question code
994 * @return string single question string
996 public function get_single($id) {
1008 * Take a string, and wrap it in a CDATA secion, if that is required to make
1009 * the output XML valid.
1010 * @param string $string a string
1011 * @return string the string, wrapped in CDATA if necessary.
1013 public function xml_escape($string) {
1014 if (!empty($string) && htmlspecialchars($string) != $string) {
1015 return "<![CDATA[{$string}]]>";
1022 * Generates <text></text> tags, processing raw text therein
1023 * @param string $raw the content to output.
1024 * @param int $indent the current indent level.
1025 * @param bool $short stick it on one line.
1026 * @return string formatted text.
1028 public function writetext($raw, $indent = 0, $short = true) {
1029 $indent = str_repeat(' ', $indent);
1030 $raw = $this->xml_escape($raw);
1033 $xml = "$indent<text>$raw</text>\n";
1035 $xml = "$indent<text>\n$raw\n$indent</text>\n";
1041 protected function presave_process($content) {
1042 // Override to allow us to add xml headers and footers
1043 return '<?xml version="1.0" encoding="UTF-8"?>
1045 ' . $content . '</quiz>';
1049 * Turns question into an xml segment
1050 * @param object $question the question data.
1051 * @return string xml segment
1053 public function writequestion($question) {
1054 global $CFG, $OUTPUT;
1056 $fs = get_file_storage();
1057 $contextid = $question->contextid;
1058 // Get files used by the questiontext.
1059 $question->questiontextfiles = $fs->get_area_files(
1060 $contextid, 'question', 'questiontext', $question->id);
1061 // Get files used by the generalfeedback.
1062 $question->generalfeedbackfiles = $fs->get_area_files(
1063 $contextid, 'question', 'generalfeedback', $question->id);
1064 if (!empty($question->options->answers)) {
1065 foreach ($question->options->answers as $answer) {
1066 $answer->answerfiles = $fs->get_area_files(
1067 $contextid, 'question', 'answer', $answer->id);
1068 $answer->feedbackfiles = $fs->get_area_files(
1069 $contextid, 'question', 'answerfeedback', $answer->id);
1075 // Add a comment linking this to the original question id.
1076 $expout .= "<!-- question: $question->id -->\n";
1078 // Check question type
1079 $questiontype = $this->get_qtype($question->qtype);
1081 // Categories are a special case.
1082 if ($question->qtype == 'category') {
1083 $categorypath = $this->writetext($question->category);
1084 $expout .= " <question type=\"category\">\n";
1085 $expout .= " <category>\n";
1086 $expout .= " $categorypath\n";
1087 $expout .= " </category>\n";
1088 $expout .= " </question>\n";
1092 // Now we know we are are handing a real question.
1093 // Output the generic information.
1094 $expout .= " <question type=\"$questiontype\">\n";
1095 $expout .= " <name>\n";
1096 $expout .= $this->writetext($question->name, 3);
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 if ($question->qtype != 'multianswer') {
1107 $expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
1109 $expout .= " <penalty>{$question->penalty}</penalty>\n";
1110 $expout .= " <hidden>{$question->hidden}</hidden>\n";
1112 // The rest of the output depends on question type.
1113 switch($question->qtype) {
1115 // not a qtype really - dummy used for category switching
1119 $trueanswer = $question->options->answers[$question->options->trueanswer];
1120 $trueanswer->answer = 'true';
1121 $expout .= $this->write_answer($trueanswer);
1123 $falseanswer = $question->options->answers[$question->options->falseanswer];
1124 $falseanswer->answer = 'false';
1125 $expout .= $this->write_answer($falseanswer);
1129 $expout .= " <single>" . $this->get_single($question->options->single) .
1131 $expout .= " <shuffleanswers>" .
1132 $this->get_single($question->options->shuffleanswers) .
1133 "</shuffleanswers>\n";
1134 $expout .= " <answernumbering>" . $question->options->answernumbering .
1135 "</answernumbering>\n";
1136 $expout .= $this->write_combined_feedback($question->options);
1137 $expout .= $this->write_answers($question->options->answers);
1141 $expout .= " <usecase>{$question->options->usecase}</usecase>\n";
1142 $expout .= $this->write_answers($question->options->answers);
1146 foreach ($question->options->answers as $answer) {
1147 $expout .= $this->write_answer($answer,
1148 " <tolerance>$answer->tolerance</tolerance>\n");
1151 $units = $question->options->units;
1152 if (count($units)) {
1153 $expout .= "<units>\n";
1154 foreach ($units as $unit) {
1155 $expout .= " <unit>\n";
1156 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1157 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1158 $expout .= " </unit>\n";
1160 $expout .= "</units>\n";
1162 if (isset($question->options->unitgradingtype)) {
1163 $expout .= " <unitgradingtype>" . $question->options->unitgradingtype .
1164 "</unitgradingtype>\n";
1166 if (isset($question->options->unitpenalty)) {
1167 $expout .= " <unitpenalty>{$question->options->unitpenalty}</unitpenalty>\n";
1169 if (isset($question->options->showunits)) {
1170 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1172 if (isset($question->options->unitsleft)) {
1173 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1175 if (!empty($question->options->instructionsformat)) {
1176 $files = $fs->get_area_files($contextid, 'qtype_numerical',
1177 'instruction', $question->id);
1178 $expout .= " <instructions " .
1179 $this->format($question->options->instructionsformat) . ">\n";
1180 $expout .= $this->writetext($question->options->instructions, 3);
1181 $expout .= $this->writefiles($files);
1182 $expout .= " </instructions>\n";
1187 $expout .= " <shuffleanswers>" .
1188 $this->get_single($question->options->shuffleanswers) .
1189 "</shuffleanswers>\n";
1190 $expout .= $this->write_combined_feedback($question->options);
1191 foreach ($question->options->subquestions as $subquestion) {
1192 $files = $fs->get_area_files($contextid, 'qtype_match',
1193 'subquestion', $subquestion->id);
1194 $expout .= " <subquestion " .
1195 $this->format($subquestion->questiontextformat) . ">\n";
1196 $expout .= $this->writetext($subquestion->questiontext, 3);
1197 $expout .= $this->writefiles($files);
1198 $expout .= " <answer>\n";
1199 $expout .= $this->writetext($subquestion->answertext, 4);
1200 $expout .= " </answer>\n";
1201 $expout .= " </subquestion>\n";
1206 // Nothing else to do.
1210 foreach ($question->options->questions as $index => $subq) {
1211 $expout = preg_replace('~{#' . $index . '}~', $subq->questiontext, $expout);
1216 $expout .= " <responseformat>" . $question->options->responseformat .
1217 "</responseformat>\n";
1218 $expout .= " <responsefieldlines>" . $question->options->responsefieldlines .
1219 "</responsefieldlines>\n";
1220 $expout .= " <attachments>" . $question->options->attachments .
1222 $expout .= " <graderinfo " .
1223 $this->format($question->options->graderinfoformat) . ">\n";
1224 $expout .= $this->writetext($question->options->graderinfo, 3);
1225 $expout .= $this->writefiles($fs->get_area_files($contextid, 'qtype_essay',
1226 'graderinfo', $question->id));
1227 $expout .= " </graderinfo>\n";
1231 case 'calculatedsimple':
1232 case 'calculatedmulti':
1233 $expout .= " <synchronize>{$question->options->synchronize}</synchronize>\n";
1234 $expout .= " <single>{$question->options->single}</single>\n";
1235 $expout .= " <answernumbering>" . $question->options->answernumbering .
1236 "</answernumbering>\n";
1237 $expout .= " <shuffleanswers>" . $question->options->shuffleanswers .
1238 "</shuffleanswers>\n";
1240 $component = 'qtype_' . $question->qtype;
1241 $files = $fs->get_area_files($contextid, $component,
1242 'correctfeedback', $question->id);
1243 $expout .= " <correctfeedback>\n";
1244 $expout .= $this->writetext($question->options->correctfeedback, 3);
1245 $expout .= $this->writefiles($files);
1246 $expout .= " </correctfeedback>\n";
1248 $files = $fs->get_area_files($contextid, $component,
1249 'partiallycorrectfeedback', $question->id);
1250 $expout .= " <partiallycorrectfeedback>\n";
1251 $expout .= $this->writetext($question->options->partiallycorrectfeedback, 3);
1252 $expout .= $this->writefiles($files);
1253 $expout .= " </partiallycorrectfeedback>\n";
1255 $files = $fs->get_area_files($contextid, $component,
1256 'incorrectfeedback', $question->id);
1257 $expout .= " <incorrectfeedback>\n";
1258 $expout .= $this->writetext($question->options->incorrectfeedback, 3);
1259 $expout .= $this->writefiles($files);
1260 $expout .= " </incorrectfeedback>\n";
1262 foreach ($question->options->answers as $answer) {
1263 $percent = 100 * $answer->fraction;
1264 $expout .= "<answer fraction=\"$percent\">\n";
1265 // "<text/>" tags are an added feature, old files won't have them
1266 $expout .= " <text>{$answer->answer}</text>\n";
1267 $expout .= " <tolerance>{$answer->tolerance}</tolerance>\n";
1268 $expout .= " <tolerancetype>{$answer->tolerancetype}</tolerancetype>\n";
1269 $expout .= " <correctanswerformat>" .
1270 $answer->correctanswerformat . "</correctanswerformat>\n";
1271 $expout .= " <correctanswerlength>" .
1272 $answer->correctanswerlength . "</correctanswerlength>\n";
1273 $expout .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1274 $files = $fs->get_area_files($contextid, $component,
1275 'instruction', $question->id);
1276 $expout .= $this->writetext($answer->feedback);
1277 $expout .= $this->writefiles($answer->feedbackfiles);
1278 $expout .= " </feedback>\n";
1279 $expout .= "</answer>\n";
1281 if (isset($question->options->unitgradingtype)) {
1282 $expout .= " <unitgradingtype>" .
1283 $question->options->unitgradingtype . "</unitgradingtype>\n";
1285 if (isset($question->options->unitpenalty)) {
1286 $expout .= " <unitpenalty>" .
1287 $question->options->unitpenalty . "</unitpenalty>\n";
1289 if (isset($question->options->showunits)) {
1290 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1292 if (isset($question->options->unitsleft)) {
1293 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1296 if (isset($question->options->instructionsformat)) {
1297 $files = $fs->get_area_files($contextid, $component,
1298 'instruction', $question->id);
1299 $expout .= " <instructions " .
1300 $this->format($question->options->instructionsformat) . ">\n";
1301 $expout .= $this->writetext($question->options->instructions, 3);
1302 $expout .= $this->writefiles($files);
1303 $expout .= " </instructions>\n";
1306 if (isset($question->options->units)) {
1307 $units = $question->options->units;
1308 if (count($units)) {
1309 $expout .= "<units>\n";
1310 foreach ($units as $unit) {
1311 $expout .= " <unit>\n";
1312 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1313 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1314 $expout .= " </unit>\n";
1316 $expout .= "</units>\n";
1320 // The tag $question->export_process has been set so we get all the
1321 // data items in the database from the function
1322 // qtype_calculated::get_question_options calculatedsimple defaults
1324 if (isset($question->options->datasets) && count($question->options->datasets)) {
1325 $expout .= "<dataset_definitions>\n";
1326 foreach ($question->options->datasets as $def) {
1327 $expout .= "<dataset_definition>\n";
1328 $expout .= " <status>".$this->writetext($def->status)."</status>\n";
1329 $expout .= " <name>".$this->writetext($def->name)."</name>\n";
1330 if ($question->qtype == CALCULATED) {
1331 $expout .= " <type>calculated</type>\n";
1333 $expout .= " <type>calculatedsimple</type>\n";
1335 $expout .= " <distribution>" . $this->writetext($def->distribution) .
1336 "</distribution>\n";
1337 $expout .= " <minimum>" . $this->writetext($def->minimum) .
1339 $expout .= " <maximum>" . $this->writetext($def->maximum) .
1341 $expout .= " <decimals>" . $this->writetext($def->decimals) .
1343 $expout .= " <itemcount>$def->itemcount</itemcount>\n";
1344 if ($def->itemcount > 0) {
1345 $expout .= " <dataset_items>\n";
1346 foreach ($def->items as $item) {
1347 $expout .= " <dataset_item>\n";
1348 $expout .= " <number>".$item->itemnumber."</number>\n";
1349 $expout .= " <value>".$item->value."</value>\n";
1350 $expout .= " </dataset_item>\n";
1352 $expout .= " </dataset_items>\n";
1353 $expout .= " <number_of_items>" . $def->number_of_items .
1354 "</number_of_items>\n";
1356 $expout .= "</dataset_definition>\n";
1358 $expout .= "</dataset_definitions>\n";
1363 // try support by optional plugin
1364 if (!$data = $this->try_exporting_using_qtypes($question->qtype, $question)) {
1365 notify(get_string('unsupportedexport', 'qformat_xml', $question->qtype));
1370 // Output any hints.
1371 $expout .= $this->write_hints($question);
1373 // Write the question tags.
1374 if (!empty($CFG->usetags)) {
1375 require_once($CFG->dirroot.'/tag/lib.php');
1376 $tags = tag_get_tags_array('question', $question->id);
1377 if (!empty($tags)) {
1378 $expout .= " <tags>\n";
1379 foreach ($tags as $tag) {
1380 $expout .= " <tag>" . $this->writetext($tag, 0, true) . "</tag>\n";
1382 $expout .= " </tags>\n";
1386 // close the question tag
1387 $expout .= " </question>\n";
1392 public function write_answers($answers) {
1393 if (empty($answers)) {
1397 foreach ($answers as $answer) {
1398 $output .= $this->write_answer($answer);
1403 public function write_answer($answer, $extra = '') {
1404 $percent = $answer->fraction * 100;
1406 $output .= " <answer fraction=\"$percent\" {$this->format($answer->answerformat)}>\n";
1407 $output .= $this->writetext($answer->answer, 3);
1408 $output .= $this->writefiles($answer->answerfiles);
1409 $output .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1410 $output .= $this->writetext($answer->feedback, 4);
1411 $output .= $this->writefiles($answer->feedbackfiles);
1412 $output .= " </feedback>\n";
1414 $output .= " </answer>\n";
1418 public function write_hints($question) {
1419 if (empty($question->hints)) {
1424 foreach ($question->hints as $hint) {
1425 $output .= $this->write_hint($hint);
1431 * @param unknown_type $format a FORMAT_... constant.
1432 * @return string the attribute to add to an XML tag.
1434 protected function format($format) {
1435 return 'format="' . $this->get_format($format) . '"';
1438 public function write_hint($hint) {
1440 $output .= " <hint {$this->format($hint->hintformat)}>\n";
1441 $output .= ' ' . $this->writetext($hint->hint);
1442 if (!empty($hint->shownumcorrect)) {
1443 $output .= " <shownumcorrect/>\n";
1445 if (!empty($hint->clearwrong)) {
1446 $output .= " <clearwrong/>\n";
1448 if (!empty($hint->options)) {
1449 $output .= ' <options>' . $this->xml_escape($hint->options) . "</options>\n";
1451 $output .= " </hint>\n";
1455 public function write_combined_feedback($questionoptions) {
1456 $output = " <correctfeedback {$this->format($questionoptions->correctfeedbackformat)}>
1457 {$this->writetext($questionoptions->correctfeedback)} </correctfeedback>
1458 <partiallycorrectfeedback {$this->format($questionoptions->partiallycorrectfeedbackformat)}>
1459 {$this->writetext($questionoptions->partiallycorrectfeedback)} </partiallycorrectfeedback>
1460 <incorrectfeedback {$this->format($questionoptions->incorrectfeedbackformat)}>
1461 {$this->writetext($questionoptions->incorrectfeedback)} </incorrectfeedback>\n";
1462 if (!empty($questionoptions->shownumcorrect)) {
1463 $output .= " <shownumcorrect/>\n";