MDL-61410 question: fix unit tests failures
[moodle.git] / question / format / xml / format.php
CommitLineData
aeb15530 1<?php
688d8753
TH
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
84769fd8 8//
688d8753
TH
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.
84769fd8 13//
688d8753
TH
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/>.
16
17/**
49e2bba7 18 * Code for exporting questions as Moodle XML.
688d8753 19 *
d2fd7262 20 * @package qformat_xml
688d8753 21 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
7764183a 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
688d8753
TH
23 */
24
25
a17b297d
TH
26defined('MOODLE_INTERNAL') || die();
27
49e2bba7 28require_once($CFG->libdir . '/xmlize.php');
19f7bb5b
TH
29if (!class_exists('qformat_default')) {
30 // This is ugly, but this class is also (ab)used by mod/lesson, which defines
31 // a different base class in mod/lesson/format.php. Thefore, we can only
32 // include the proper base class conditionally like this. (We have to include
33 // the base class like this, otherwise it breaks third-party question types.)
34 // This may be reviewd, and a better fix found one day.
35 require_once($CFG->dirroot . '/question/format.php');
36}
49e2bba7
TH
37
38
41a89a07 39/**
688d8753
TH
40 * Importer for Moodle XML question format.
41 *
42 * See http://docs.moodle.org/en/Moodle_XML_format for a description of the format.
43 *
44 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
7764183a 45 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41a89a07 46 */
f5565b69 47class qformat_xml extends qformat_default {
84769fd8 48
c7df5006 49 public function provide_import() {
84769fd8 50 return true;
51 }
52
c7df5006 53 public function provide_export() {
84769fd8 54 return true;
55 }
56
c7df5006 57 public function mime_type() {
46732124
TH
58 return 'application/xml';
59 }
60
d2fd7262 61 // IMPORT FUNCTIONS START HERE.
84769fd8 62
88bc20c3 63 /**
c81415c7 64 * Translate human readable format name
65 * into internal Moodle code number
6e557c08 66 * @param string name format name from xml file
f7970e3c 67 * @return int Moodle format code
c81415c7 68 */
4a84c94b 69 public function trans_format($name) {
88bc20c3 70 $name = trim($name);
71
2ed80177 72 if ($name == 'moodle_auto_format') {
49e2bba7 73 return FORMAT_MOODLE;
2ed80177 74 } else if ($name == 'html') {
49e2bba7 75 return FORMAT_HTML;
2ed80177 76 } else if ($name == 'plain_text') {
49e2bba7 77 return FORMAT_PLAIN;
2ed80177 78 } else if ($name == 'wiki_like') {
49e2bba7 79 return FORMAT_WIKI;
2ed80177 80 } else if ($name == 'markdown') {
49e2bba7 81 return FORMAT_MARKDOWN;
2ed80177 82 } else {
fe5f4121
TH
83 debugging("Unrecognised text format '{$name}' in the import file. Assuming 'html'.");
84 return FORMAT_HTML;
84769fd8 85 }
84769fd8 86 }
87
6e557c08 88 /**
c81415c7 89 * Translate human readable single answer option
90 * to internal code number
6e557c08 91 * @param string name true/false
f7970e3c 92 * @return int internal code number
c81415c7 93 */
c7df5006 94 public function trans_single($name) {
2da44816 95 $name = trim($name);
96 if ($name == "false" || !$name) {
97 return 0;
98 } else {
99 return 1;
100 }
84769fd8 101 }
102
6e557c08 103 /**
c81415c7 104 * process text string from xml file
6e557c08 105 * @param array $text bit of xml tree after ['text']
49e2bba7 106 * @return string processed text.
c81415c7 107 */
c7df5006 108 public function import_text($text) {
d2fd7262 109 // Quick sanity check.
17102269 110 if (empty($text)) {
111 return '';
112 }
84769fd8 113 $data = $text[0]['#'];
294ce987 114 return trim($data);
84769fd8 115 }
116
46013523 117 /**
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
88bc20c3 121 * @param array path path to node expressed as array
122 * @param mixed default
f7970e3c 123 * @param bool istext process as text
46013523 124 * @param string error if set value must exist, return false and issue message if not
125 * @return mixed value
126 */
c7df5006 127 public function getpath($xml, $path, $default, $istext=false, $error='') {
46013523 128 foreach ($path as $index) {
228b6f6b 129 if (!isset($xml[$index])) {
46013523 130 if (!empty($error)) {
49e2bba7 131 $this->error($error);
46013523 132 return false;
133 } else {
134 return $default;
135 }
136 }
49e2bba7
TH
137
138 $xml = $xml[$index];
46013523 139 }
49e2bba7 140
46013523 141 if ($istext) {
fc22da99 142 if (!is_string($xml)) {
49e2bba7 143 $this->error(get_string('invalidxml', 'qformat_xml'));
fc22da99 144 }
cde2709a 145 $xml = trim($xml);
46013523 146 }
147
148 return $xml;
149 }
150
2387d1cd
JMV
151 public function import_text_with_files($data, $path, $defaultvalue = '', $defaultformat = 'html') {
152 $field = array();
153 $field['text'] = $this->getpath($data,
154 array_merge($path, array('#', 'text', 0, '#')), $defaultvalue, true);
155 $field['format'] = $this->trans_format($this->getpath($data,
156 array_merge($path, array('@', 'format')), $defaultformat));
157 $itemid = $this->import_files_as_draft($this->getpath($data,
158 array_merge($path, array('#', 'file')), array(), false));
159 if (!empty($itemid)) {
160 $field['itemid'] = $itemid;
161 }
162 return $field;
163 }
164
165 public function import_files_as_draft($xml) {
166 global $USER;
167 if (empty($xml)) {
168 return null;
169 }
170 $fs = get_file_storage();
171 $itemid = file_get_unused_draft_itemid();
624ff5ba 172 $filepaths = array();
2387d1cd 173 foreach ($xml as $file) {
624ff5ba
JMV
174 $filename = $this->getpath($file, array('@', 'name'), '', true);
175 $filepath = $this->getpath($file, array('@', 'path'), '/', true);
176 $fullpath = $filepath . $filename;
177 if (in_array($fullpath, $filepaths)) {
178 debugging('Duplicate file in XML: ' . $fullpath, DEBUG_DEVELOPER);
ded5cddb
JMV
179 continue;
180 }
2387d1cd
JMV
181 $filerecord = array(
182 'contextid' => context_user::instance($USER->id)->id,
183 'component' => 'user',
184 'filearea' => 'draft',
185 'itemid' => $itemid,
624ff5ba 186 'filepath' => $filepath,
ded5cddb 187 'filename' => $filename,
2387d1cd
JMV
188 );
189 $fs->create_file_from_string($filerecord, base64_decode($file['#']));
624ff5ba 190 $filepaths[] = $fullpath;
2387d1cd
JMV
191 }
192 return $itemid;
193 }
46013523 194
6e557c08 195 /**
c81415c7 196 * import parts of question common to all types
e7ef42f5 197 * @param $question array question question array from xml tree
6e557c08 198 * @return object question object
c81415c7 199 */
c7df5006 200 public function import_headers($question) {
dcd4464e 201 global $USER;
4f290077 202
d2fd7262 203 // This routine initialises the question object.
5bed54e1 204 $qo = $this->defaultquestion();
84769fd8 205
d2fd7262 206 // Question name.
cacb8fa0 207 $qo->name = $this->clean_question_name($this->getpath($question,
49e2bba7 208 array('#', 'name', 0, '#', 'text', 0, '#'), '', true,
cacb8fa0 209 get_string('xmlimportnoname', 'qformat_xml')));
2387d1cd
JMV
210 $questiontext = $this->import_text_with_files($question,
211 array('#', 'questiontext', 0));
212 $qo->questiontext = $questiontext['text'];
213 $qo->questiontextformat = $questiontext['format'];
214 if (!empty($questiontext['itemid'])) {
215 $qo->questiontextitemid = $questiontext['itemid'];
216 }
06f1bd03
TH
217 // Backwards compatibility, deal with the old image tag.
218 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
219 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
220 if ($filedata && $filename) {
2387d1cd
JMV
221 $fs = get_file_storage();
222 if (empty($qo->questiontextitemid)) {
223 $qo->questiontextitemid = file_get_unused_draft_itemid();
224 }
225 $filename = clean_param(str_replace('/', '_', $filename), PARAM_FILE);
226 $filerecord = array(
227 'contextid' => context_user::instance($USER->id)->id,
228 'component' => 'user',
229 'filearea' => 'draft',
230 'itemid' => $qo->questiontextitemid,
231 'filepath' => '/',
232 'filename' => $filename,
233 );
234 $fs->create_file_from_string($filerecord, base64_decode($filedata));
235 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
06f1bd03
TH
236 }
237
d2fd7262 238 // Restore files in generalfeedback.
2387d1cd
JMV
239 $generalfeedback = $this->import_text_with_files($question,
240 array('#', 'generalfeedback', 0), $qo->generalfeedback, $this->get_format($qo->questiontextformat));
241 $qo->generalfeedback = $generalfeedback['text'];
242 $qo->generalfeedbackformat = $generalfeedback['format'];
243 if (!empty($generalfeedback['itemid'])) {
244 $qo->generalfeedbackitemid = $generalfeedback['itemid'];
245 }
cde2709a 246
79e25fb4
TH
247 $qo->defaultmark = $this->getpath($question,
248 array('#', 'defaultgrade', 0, '#'), $qo->defaultmark);
249 $qo->penalty = $this->getpath($question,
250 array('#', 'penalty', 0, '#'), $qo->penalty);
49e2bba7 251
d2fd7262 252 // Fix problematic rounding from old files.
49e2bba7
TH
253 if (abs($qo->penalty - 0.3333333) < 0.005) {
254 $qo->penalty = 0.3333333;
255 }
84769fd8 256
4f290077 257 // Read the question tags.
dcd4464e 258 $this->import_question_tags($qo, $question);
4f290077 259
84769fd8 260 return $qo;
261 }
262
6e557c08 263 /**
49e2bba7 264 * Import the common parts of a single answer
6e557c08 265 * @param array answer xml tree for single answer
fe5f4121
TH
266 * @param bool $withanswerfiles if true, the answers are HTML (or $defaultformat)
267 * and so may contain files, otherwise the answers are plain text.
268 * @param array Default text format for the feedback, and the answers if $withanswerfiles
269 * is true.
6e557c08 270 * @return object answer object
88bc20c3 271 */
fe5f4121 272 public function import_answer($answer, $withanswerfiles = false, $defaultformat = 'html') {
2a6c5c52 273 $ans = new stdClass();
274
2a6c5c52 275 if ($withanswerfiles) {
2387d1cd 276 $ans->answer = $this->import_text_with_files($answer, array(), '', $defaultformat);
fe5f4121 277 } else {
2387d1cd
JMV
278 $ans->answer = array();
279 $ans->answer['text'] = $this->getpath($answer, array('#', 'text', 0, '#'), '', true);
fe5f4121 280 $ans->answer['format'] = FORMAT_PLAIN;
2a6c5c52 281 }
cde2709a 282
2387d1cd 283 $ans->feedback = $this->import_text_with_files($answer, array('#', 'feedback', 0), '', $defaultformat);
cde2709a 284
2a6c5c52 285 $ans->fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
84769fd8 286
84769fd8 287 return $ans;
288 }
289
49e2bba7
TH
290 /**
291 * Import the common overall feedback fields.
292 * @param object $question the part of the XML relating to this question.
293 * @param object $qo the question data to add the fields to.
f7970e3c 294 * @param bool $withshownumpartscorrect include the shownumcorrect field.
49e2bba7
TH
295 */
296 public function import_combined_feedback($qo, $questionxml, $withshownumpartscorrect = false) {
79e25fb4
TH
297 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
298 foreach ($fields as $field) {
2387d1cd
JMV
299 $qo->$field = $this->import_text_with_files($questionxml,
300 array('#', $field, 0), '', $this->get_format($qo->questiontextformat));
fe041243 301 }
49e2bba7
TH
302
303 if ($withshownumpartscorrect) {
304 $qo->shownumcorrect = array_key_exists('shownumcorrect', $questionxml['#']);
305
d2fd7262 306 // Backwards compatibility.
49e2bba7
TH
307 if (array_key_exists('correctresponsesfeedback', $questionxml['#'])) {
308 $qo->shownumcorrect = $this->trans_single($this->getpath($questionxml,
309 array('#', 'correctresponsesfeedback', 0, '#'), 1));
310 }
311 }
312 }
313
314 /**
315 * Import a question hint
316 * @param array $hintxml hint xml fragment.
fe5f4121 317 * @param string $defaultformat the text format to assume for hints that do not specify.
49e2bba7
TH
318 * @return object hint for storing in the database.
319 */
fe5f4121 320 public function import_hint($hintxml, $defaultformat) {
2387d1cd 321 $hint = new stdClass();
49e2bba7 322 if (array_key_exists('hintcontent', $hintxml['#'])) {
d2fd7262 323 // Backwards compatibility.
49e2bba7 324
2387d1cd
JMV
325 $hint->hint = $this->import_text_with_files($hintxml,
326 array('#', 'hintcontent', 0), '', $defaultformat);
327
49e2bba7
TH
328 $hint->shownumcorrect = $this->getpath($hintxml,
329 array('#', 'statenumberofcorrectresponses', 0, '#'), 0);
330 $hint->clearwrong = $this->getpath($hintxml,
331 array('#', 'clearincorrectresponses', 0, '#'), 0);
332 $hint->options = $this->getpath($hintxml,
333 array('#', 'showfeedbacktoresponses', 0, '#'), 0);
334
335 return $hint;
336 }
2387d1cd 337 $hint->hint = $this->import_text_with_files($hintxml, array(), '', $defaultformat);
49e2bba7
TH
338 $hint->shownumcorrect = array_key_exists('shownumcorrect', $hintxml['#']);
339 $hint->clearwrong = array_key_exists('clearwrong', $hintxml['#']);
79e25fb4 340 $hint->options = $this->getpath($hintxml, array('#', 'options', 0, '#'), '', true);
49e2bba7
TH
341
342 return $hint;
343 }
344
345 /**
346 * Import all the question hints
347 *
348 * @param object $qo the question data that is being constructed.
fe5f4121
TH
349 * @param array $questionxml The xml representing the question.
350 * @param bool $withparts whether the extra fields relating to parts should be imported.
351 * @param bool $withoptions whether the extra options field should be imported.
352 * @param string $defaultformat the text format to assume for hints that do not specify.
353 * @return array of objects representing the hints in the file.
49e2bba7 354 */
fe5f4121
TH
355 public function import_hints($qo, $questionxml, $withparts = false,
356 $withoptions = false, $defaultformat = 'html') {
49e2bba7
TH
357 if (!isset($questionxml['#']['hint'])) {
358 return;
359 }
fe041243 360
49e2bba7 361 foreach ($questionxml['#']['hint'] as $hintxml) {
fe5f4121 362 $hint = $this->import_hint($hintxml, $defaultformat);
49e2bba7
TH
363 $qo->hint[] = $hint->hint;
364
365 if ($withparts) {
366 $qo->hintshownumcorrect[] = $hint->shownumcorrect;
367 $qo->hintclearwrong[] = $hint->clearwrong;
368 }
369
370 if ($withoptions) {
371 $qo->hintoptions[] = $hint->options;
372 }
373 }
374 }
375
dcd4464e
EC
376 /**
377 * Import all the question tags
378 *
379 * @param object $qo the question data that is being constructed.
380 * @param array $questionxml The xml representing the question.
381 * @return array of objects representing the tags in the file.
382 */
383 public function import_question_tags($qo, $questionxml) {
384 global $CFG;
385
13596866
SL
386 if (core_tag_tag::is_enabled('core_question', 'question')) {
387
388 $qo->tags = [];
389 if (!empty($questionxml['#']['tags'][0]['#']['tag'])) {
390 foreach ($questionxml['#']['tags'][0]['#']['tag'] as $tagdata) {
391 $qo->tags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true);
392 }
393 }
394
395 $qo->coursetags = [];
396 if (!empty($questionxml['#']['coursetags'][0]['#']['tag'])) {
397 foreach ($questionxml['#']['coursetags'][0]['#']['tag'] as $tagdata) {
398 $qo->coursetags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true);
399 }
dcd4464e
EC
400 }
401 }
402 }
403
c73c9836
TH
404 /**
405 * Import files from a node in the XML.
406 * @param array $xml an array of <file> nodes from the the parsed XML.
407 * @return array of things representing files - in the form that save_question expects.
408 */
409 public function import_files($xml) {
410 $files = array();
411 foreach ($xml as $file) {
412 $data = new stdClass();
413 $data->content = $file['#'];
414 $data->encoding = $file['@']['encoding'];
415 $data->name = $file['@']['name'];
416 $files[] = $data;
417 }
418 return $files;
419 }
420
6e557c08 421 /**
88bc20c3 422 * import multiple choice question
6e557c08 423 * @param array question question array from xml tree
424 * @return object question object
c81415c7 425 */
c7df5006 426 public function import_multichoice($question) {
d2fd7262 427 // Get common parts.
cde2709a 428 $qo = $this->import_headers($question);
84769fd8 429
d2fd7262 430 // Header parts particular to multichoice.
e231a3ff 431 $qo->qtype = 'multichoice';
49e2bba7
TH
432 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
433 $qo->single = $this->trans_single($single);
79e25fb4
TH
434 $shuffleanswers = $this->getpath($question,
435 array('#', 'shuffleanswers', 0, '#'), 'false');
436 $qo->answernumbering = $this->getpath($question,
437 array('#', 'answernumbering', 0, '#'), 'abc');
2da44816 438 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
cde2709a 439
79e25fb4
TH
440 // There was a time on the 1.8 branch when it could output an empty
441 // answernumbering tag, so fix up any found.
c0ffeb39 442 if (empty($qo->answernumbering)) {
443 $qo->answernumbering = 'abc';
444 }
445
d2fd7262 446 // Run through the answers.
88bc20c3 447 $answers = $question['#']['answer'];
49e2bba7 448 $acount = 0;
84769fd8 449 foreach ($answers as $answer) {
fe5f4121 450 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
49e2bba7
TH
451 $qo->answer[$acount] = $ans->answer;
452 $qo->fraction[$acount] = $ans->fraction;
453 $qo->feedback[$acount] = $ans->feedback;
454 ++$acount;
84769fd8 455 }
cde2709a 456
49e2bba7 457 $this->import_combined_feedback($qo, $question, true);
fe5f4121 458 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
49e2bba7 459
84769fd8 460 return $qo;
461 }
462
6e557c08 463 /**
49e2bba7 464 * Import cloze type question
6e557c08 465 * @param array question question array from xml tree
466 * @return object question object
c81415c7 467 */
77d0f5f0 468 public function import_multianswer($question) {
c80fb6d9 469 global $USER;
a3f92b2e 470 question_bank::get_qtype('multianswer');
1f1c60c6 471
c80fb6d9
JMV
472 $questiontext = $this->import_text_with_files($question,
473 array('#', 'questiontext', 0));
c8fdd867 474 $qo = qtype_multianswer_extract_question($questiontext);
a3f92b2e 475
d2fd7262 476 // Header parts particular to multianswer.
77d0f5f0 477 $qo->qtype = 'multianswer';
08000e53
AG
478
479 // Only set the course if the data is available.
480 if (isset($this->course)) {
481 $qo->course = $this->course;
482 }
fe5f4121 483
cacb8fa0 484 $qo->name = $this->clean_question_name($this->import_text($question['#']['name'][0]['#']['text']));
fe5f4121
TH
485 $qo->questiontextformat = $questiontext['format'];
486 $qo->questiontext = $qo->questiontext['text'];
c80fb6d9
JMV
487 if (!empty($questiontext['itemid'])) {
488 $qo->questiontextitemid = $questiontext['itemid'];
2387d1cd 489 }
c80fb6d9 490
bedbfd6d
JMV
491 // Backwards compatibility, deal with the old image tag.
492 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
493 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
494 if ($filedata && $filename) {
c80fb6d9
JMV
495 $fs = get_file_storage();
496 if (empty($qo->questiontextitemid)) {
497 $qo->questiontextitemid = file_get_unused_draft_itemid();
498 }
499 $filename = clean_param(str_replace('/', '_', $filename), PARAM_FILE);
500 $filerecord = array(
501 'contextid' => context_user::instance($USER->id)->id,
502 'component' => 'user',
503 'filearea' => 'draft',
504 'itemid' => $qo->questiontextitemid,
505 'filepath' => '/',
506 'filename' => $filename,
507 );
508 $fs->create_file_from_string($filerecord, base64_decode($filedata));
509 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
bedbfd6d
JMV
510 }
511
d2fd7262 512 // Restore files in generalfeedback.
2387d1cd
JMV
513 $generalfeedback = $this->import_text_with_files($question,
514 array('#', 'generalfeedback', 0), $qo->generalfeedback, $this->get_format($qo->questiontextformat));
515 $qo->generalfeedback = $generalfeedback['text'];
516 $qo->generalfeedbackformat = $generalfeedback['format'];
517 if (!empty($generalfeedback['itemid'])) {
518 $qo->generalfeedbackitemid = $generalfeedback['itemid'];
519 }
c73c9836 520
77d0f5f0
TH
521 $qo->penalty = $this->getpath($question,
522 array('#', 'penalty', 0, '#'), $this->defaultquestion()->penalty);
d2fd7262 523 // Fix problematic rounding from old files.
77d0f5f0
TH
524 if (abs($qo->penalty - 0.3333333) < 0.005) {
525 $qo->penalty = 0.3333333;
526 }
527
e9af6091 528 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
dcd4464e 529 $this->import_question_tags($qo, $question);
49e2bba7 530
7b8bc256 531 return $qo;
532 }
533
6e557c08 534 /**
49e2bba7 535 * Import true/false type question
6e557c08 536 * @param array question question array from xml tree
537 * @return object question object
c81415c7 538 */
c7df5006 539 public function import_truefalse($question) {
d2fd7262 540 // Get common parts.
fef8f84e 541 global $OUTPUT;
49e2bba7 542 $qo = $this->import_headers($question);
84769fd8 543
d2fd7262 544 // Header parts particular to true/false.
e231a3ff 545 $qo->qtype = 'truefalse';
84769fd8 546
3246ed33 547 // In the past, it used to be assumed that the two answers were in the file
548 // true first, then false. Howevever that was not always true. Now, we
549 // try to match on the answer text, but in old exports, this will be a localised
550 // string, so if we don't find true or false, we fall back to the old system.
551 $first = true;
552 $warning = false;
553 foreach ($question['#']['answer'] as $answer) {
79e25fb4
TH
554 $answertext = $this->getpath($answer,
555 array('#', 'text', 0, '#'), '', true);
2387d1cd
JMV
556 $feedback = $this->import_text_with_files($answer,
557 array('#', 'feedback', 0), '', $this->get_format($qo->questiontextformat));
a9081ce6 558
3246ed33 559 if ($answertext != 'true' && $answertext != 'false') {
49e2bba7 560 // Old style file, assume order is true/false.
3246ed33 561 $warning = true;
49e2bba7
TH
562 if ($first) {
563 $answertext = 'true';
564 } else {
565 $answertext = 'false';
566 }
88bc20c3 567 }
49e2bba7 568
3246ed33 569 if ($answertext == 'true') {
570 $qo->answer = ($answer['@']['fraction'] == 100);
7939a4a0 571 $qo->correctanswer = $qo->answer;
2387d1cd 572 $qo->feedbacktrue = $feedback;
3246ed33 573 } else {
574 $qo->answer = ($answer['@']['fraction'] != 100);
7939a4a0 575 $qo->correctanswer = $qo->answer;
2387d1cd 576 $qo->feedbackfalse = $feedback;
3246ed33 577 }
578 $first = false;
84769fd8 579 }
3246ed33 580
581 if ($warning) {
0ff4bd08 582 $a = new stdClass();
55c54868 583 $a->questiontext = $qo->questiontext;
5e8a85aa
TH
584 $a->answer = get_string($qo->correctanswer ? 'true' : 'false', 'qtype_truefalse');
585 echo $OUTPUT->notification(get_string('truefalseimporterror', 'qformat_xml', $a));
84769fd8 586 }
49e2bba7 587
fe5f4121 588 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 589
84769fd8 590 return $qo;
591 }
592
6e557c08 593 /**
49e2bba7 594 * Import short answer type question
6e557c08 595 * @param array question question array from xml tree
596 * @return object question object
c81415c7 597 */
c7df5006 598 public function import_shortanswer($question) {
d2fd7262 599 // Get common parts.
49e2bba7 600 $qo = $this->import_headers($question);
84769fd8 601
d2fd7262 602 // Header parts particular to shortanswer.
e231a3ff 603 $qo->qtype = 'shortanswer';
84769fd8 604
d2fd7262 605 // Get usecase.
49e2bba7 606 $qo->usecase = $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase);
84769fd8 607
d2fd7262 608 // Run through the answers.
88bc20c3 609 $answers = $question['#']['answer'];
49e2bba7 610 $acount = 0;
84769fd8 611 foreach ($answers as $answer) {
fe5f4121 612 $ans = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
fe041243 613 $qo->answer[$acount] = $ans->answer['text'];
49e2bba7
TH
614 $qo->fraction[$acount] = $ans->fraction;
615 $qo->feedback[$acount] = $ans->feedback;
616 ++$acount;
84769fd8 617 }
618
fe5f4121 619 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 620
84769fd8 621 return $qo;
622 }
88bc20c3 623
6e557c08 624 /**
49e2bba7 625 * Import description type question
6e557c08 626 * @param array question question array from xml tree
627 * @return object question object
c81415c7 628 */
c7df5006 629 public function import_description($question) {
d2fd7262 630 // Get common parts.
49e2bba7 631 $qo = $this->import_headers($question);
d2fd7262 632 // Header parts particular to shortanswer.
e231a3ff 633 $qo->qtype = 'description';
49e2bba7 634 $qo->defaultmark = 0;
3f5633df 635 $qo->length = 0;
7b8bc256 636 return $qo;
637 }
84769fd8 638
6e557c08 639 /**
49e2bba7 640 * Import numerical type question
6e557c08 641 * @param array question question array from xml tree
642 * @return object question object
c81415c7 643 */
c7df5006 644 public function import_numerical($question) {
d2fd7262 645 // Get common parts.
2ed80177 646 $qo = $this->import_headers($question);
84769fd8 647
d2fd7262 648 // Header parts particular to numerical.
e231a3ff 649 $qo->qtype = 'numerical';
84769fd8 650
d2fd7262 651 // Get answers array.
84769fd8 652 $answers = $question['#']['answer'];
653 $qo->answer = array();
654 $qo->feedback = array();
655 $qo->fraction = array();
656 $qo->tolerance = array();
657 foreach ($answers as $answer) {
d2fd7262 658 // Answer outside of <text> is deprecated.
fe5f4121 659 $obj = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
69988ed4 660 $qo->answer[] = $obj->answer['text'];
81a7a02b 661 if (empty($qo->answer)) {
662 $qo->answer = '*';
55c54868 663 }
2ed80177
TH
664 $qo->feedback[] = $obj->feedback;
665 $qo->tolerance[] = $this->getpath($answer, array('#', 'tolerance', 0, '#'), 0);
55c54868 666
d2fd7262 667 // Fraction as a tag is deprecated.
2ed80177 668 $fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
79e25fb4 669 $qo->fraction[] = $this->getpath($answer,
d2fd7262 670 array('#', 'fraction', 0, '#'), $fraction); // Deprecated.
84769fd8 671 }
672
d2fd7262 673 // Get the units array.
84769fd8 674 $qo->unit = array();
49e2bba7 675 $units = $this->getpath($question, array('#', 'units', 0, '#', 'unit'), array());
81a7a02b 676 if (!empty($units)) {
a0d187bf 677 $qo->multiplier = array();
678 foreach ($units as $unit) {
79e25fb4
TH
679 $qo->multiplier[] = $this->getpath($unit, array('#', 'multiplier', 0, '#'), 1);
680 $qo->unit[] = $this->getpath($unit, array('#', 'unit_name', 0, '#'), '', true);
a0d187bf 681 }
84769fd8 682 }
79e25fb4 683 $qo->unitgradingtype = $this->getpath($question, array('#', 'unitgradingtype', 0, '#'), 0);
de529cd2
TH
684 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0.1);
685 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), null);
79e25fb4 686 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
69988ed4
TH
687 $qo->instructions['text'] = '';
688 $qo->instructions['format'] = FORMAT_HTML;
cde2709a
DC
689 $instructions = $this->getpath($question, array('#', 'instructions'), array());
690 if (!empty($instructions)) {
2387d1cd
JMV
691 $qo->instructions = $this->import_text_with_files($instructions,
692 array('0'), '', $this->get_format($qo->questiontextformat));
cde2709a 693 }
49e2bba7 694
de529cd2
TH
695 if (is_null($qo->showunits)) {
696 // Set a good default, depending on whether there are any units defined.
697 if (empty($qo->unit)) {
d2fd7262 698 $qo->showunits = 3; // This is qtype_numerical::UNITNONE, but we cannot refer to that constant here.
de529cd2 699 } else {
d2fd7262 700 $qo->showunits = 0; // This is qtype_numerical::UNITOPTIONAL, but we cannot refer to that constant here.
de529cd2
TH
701 }
702 }
703
fe5f4121 704 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 705
84769fd8 706 return $qo;
707 }
708
6e557c08 709 /**
49e2bba7 710 * Import matching type question
6e557c08 711 * @param array question question array from xml tree
712 * @return object question object
c81415c7 713 */
77d0f5f0 714 public function import_match($question) {
d2fd7262 715 // Get common parts.
2ed80177 716 $qo = $this->import_headers($question);
51bcdf28 717
d2fd7262 718 // Header parts particular to matching.
77d0f5f0 719 $qo->qtype = 'match';
49e2bba7
TH
720 $qo->shuffleanswers = $this->trans_single($this->getpath($question,
721 array('#', 'shuffleanswers', 0, '#'), 1));
51bcdf28 722
d2fd7262 723 // Run through subquestions.
51bcdf28 724 $qo->subquestions = array();
725 $qo->subanswers = array();
fe041243 726 foreach ($question['#']['subquestion'] as $subqxml) {
2387d1cd
JMV
727 $qo->subquestions[] = $this->import_text_with_files($subqxml,
728 array(), '', $this->get_format($qo->questiontextformat));
729
fe041243 730 $answers = $this->getpath($subqxml, array('#', 'answer'), array());
79e25fb4
TH
731 $qo->subanswers[] = $this->getpath($subqxml,
732 array('#', 'answer', 0, '#', 'text', 0, '#'), '', true);
51bcdf28 733 }
49e2bba7
TH
734
735 $this->import_combined_feedback($qo, $question, true);
fe5f4121 736 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
49e2bba7 737
51bcdf28 738 return $qo;
739 }
740
6e557c08 741 /**
49e2bba7 742 * Import essay type question
6e557c08 743 * @param array question question array from xml tree
744 * @return object question object
c81415c7 745 */
c7df5006 746 public function import_essay($question) {
d2fd7262 747 // Get common parts.
49e2bba7 748 $qo = $this->import_headers($question);
c81415c7 749
d2fd7262 750 // Header parts particular to essay.
e231a3ff 751 $qo->qtype = 'essay';
c81415c7 752
8a5d05ee
TH
753 $qo->responseformat = $this->getpath($question,
754 array('#', 'responseformat', 0, '#'), 'editor');
755 $qo->responsefieldlines = $this->getpath($question,
756 array('#', 'responsefieldlines', 0, '#'), 15);
a4f765eb
KT
757 $qo->responserequired = $this->getpath($question,
758 array('#', 'responserequired', 0, '#'), 1);
8a5d05ee
TH
759 $qo->attachments = $this->getpath($question,
760 array('#', 'attachments', 0, '#'), 0);
a4f765eb
KT
761 $qo->attachmentsrequired = $this->getpath($question,
762 array('#', 'attachmentsrequired', 0, '#'), 0);
2387d1cd
JMV
763 $qo->graderinfo = $this->import_text_with_files($question,
764 array('#', 'graderinfo', 0), '', $this->get_format($qo->questiontextformat));
60527d0c
JMV
765 $qo->responsetemplate['text'] = $this->getpath($question,
766 array('#', 'responsetemplate', 0, '#', 'text', 0, '#'), '', true);
767 $qo->responsetemplate['format'] = $this->trans_format($this->getpath($question,
768 array('#', 'responsetemplate', 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
c81415c7 769
770 return $qo;
771 }
84769fd8 772
79e25fb4
TH
773 /**
774 * Import a calculated question
775 * @param object $question the imported XML data.
776 */
777 public function import_calculated($question) {
725ba2a0 778
d2fd7262 779 // Get common parts.
49e2bba7 780 $qo = $this->import_headers($question);
725ba2a0 781
d2fd7262 782 // Header parts particular to calculated.
e231a3ff 783 $qo->qtype = 'calculated';
79e25fb4
TH
784 $qo->synchronize = $this->getpath($question, array('#', 'synchronize', 0, '#'), 0);
785 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
786 $qo->single = $this->trans_single($single);
787 $shuffleanswers = $this->getpath($question, array('#', 'shuffleanswers', 0, '#'), 'false');
788 $qo->answernumbering = $this->getpath($question,
789 array('#', 'answernumbering', 0, '#'), 'abc');
04d8268d 790 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
cde2709a 791
fe5f4121 792 $this->import_combined_feedback($qo, $question);
79e25fb4
TH
793
794 $qo->unitgradingtype = $this->getpath($question,
795 array('#', 'unitgradingtype', 0, '#'), 0);
de529cd2 796 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), null);
79e25fb4
TH
797 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
798 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
799 $qo->instructions = $this->getpath($question,
800 array('#', 'instructions', 0, '#', 'text', 0, '#'), '', true);
ac582c3b 801 if (!empty($instructions)) {
2387d1cd
JMV
802 $qo->instructions = $this->import_text_with_files($instructions,
803 array('0'), '', $this->get_format($qo->questiontextformat));
ac582c3b 804 }
cde2709a 805
d2fd7262 806 // Get answers array.
725ba2a0 807 $answers = $question['#']['answer'];
66de66fe 808 $qo->answer = array();
725ba2a0 809 $qo->feedback = array();
810 $qo->fraction = array();
811 $qo->tolerance = array();
812 $qo->tolerancetype = array();
813 $qo->correctanswerformat = array();
814 $qo->correctanswerlength = array();
815 $qo->feedback = array();
816 foreach ($answers as $answer) {
fe5f4121 817 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
d2fd7262 818 // Answer outside of <text> is deprecated.
cde2709a
DC
819 if (empty($ans->answer['text'])) {
820 $ans->answer['text'] = '*';
725ba2a0 821 }
66de66fe 822 $qo->answer[] = $ans->answer['text'];
cde2709a 823 $qo->feedback[] = $ans->feedback;
725ba2a0 824 $qo->tolerance[] = $answer['#']['tolerance'][0]['#'];
d2fd7262 825 // Fraction as a tag is deprecated.
725ba2a0 826 if (!empty($answer['#']['fraction'][0]['#'])) {
827 $qo->fraction[] = $answer['#']['fraction'][0]['#'];
cde2709a 828 } else {
725ba2a0 829 $qo->fraction[] = $answer['@']['fraction'] / 100;
830 }
831 $qo->tolerancetype[] = $answer['#']['tolerancetype'][0]['#'];
832 $qo->correctanswerformat[] = $answer['#']['correctanswerformat'][0]['#'];
833 $qo->correctanswerlength[] = $answer['#']['correctanswerlength'][0]['#'];
834 }
d2fd7262 835 // Get units array.
725ba2a0 836 $qo->unit = array();
837 if (isset($question['#']['units'][0]['#']['unit'])) {
838 $units = $question['#']['units'][0]['#']['unit'];
839 $qo->multiplier = array();
840 foreach ($units as $unit) {
841 $qo->multiplier[] = $unit['#']['multiplier'][0]['#'];
842 $qo->unit[] = $unit['#']['unit_name'][0]['#'];
843 }
844 }
cde2709a
DC
845 $instructions = $this->getpath($question, array('#', 'instructions'), array());
846 if (!empty($instructions)) {
2387d1cd
JMV
847 $qo->instructions = $this->import_text_with_files($instructions,
848 array('0'), '', $this->get_format($qo->questiontextformat));
cde2709a 849 }
de529cd2
TH
850
851 if (is_null($qo->unitpenalty)) {
852 // Set a good default, depending on whether there are any units defined.
853 if (empty($qo->unit)) {
d2fd7262 854 $qo->showunits = 3; // This is qtype_numerical::UNITNONE, but we cannot refer to that constant here.
de529cd2 855 } else {
d2fd7262 856 $qo->showunits = 0; // This is qtype_numerical::UNITOPTIONAL, but we cannot refer to that constant here.
de529cd2
TH
857 }
858 }
859
cde2709a
DC
860 $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition'];
861 $qo->dataset = array();
79e25fb4 862 $qo->datasetindex= 0;
725ba2a0 863 foreach ($datasets as $dataset) {
864 $qo->datasetindex++;
865 $qo->dataset[$qo->datasetindex] = new stdClass();
79e25fb4
TH
866 $qo->dataset[$qo->datasetindex]->status =
867 $this->import_text($dataset['#']['status'][0]['#']['text']);
868 $qo->dataset[$qo->datasetindex]->name =
869 $this->import_text($dataset['#']['name'][0]['#']['text']);
870 $qo->dataset[$qo->datasetindex]->type =
871 $dataset['#']['type'][0]['#'];
872 $qo->dataset[$qo->datasetindex]->distribution =
873 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
874 $qo->dataset[$qo->datasetindex]->max =
875 $this->import_text($dataset['#']['maximum'][0]['#']['text']);
876 $qo->dataset[$qo->datasetindex]->min =
877 $this->import_text($dataset['#']['minimum'][0]['#']['text']);
878 $qo->dataset[$qo->datasetindex]->length =
879 $this->import_text($dataset['#']['decimals'][0]['#']['text']);
880 $qo->dataset[$qo->datasetindex]->distribution =
881 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
725ba2a0 882 $qo->dataset[$qo->datasetindex]->itemcount = $dataset['#']['itemcount'][0]['#'];
883 $qo->dataset[$qo->datasetindex]->datasetitem = array();
884 $qo->dataset[$qo->datasetindex]->itemindex = 0;
013fa7a4
TH
885 $qo->dataset[$qo->datasetindex]->number_of_items = $this->getpath($dataset,
886 array('#', 'number_of_items', 0, '#'), 0);
887 $datasetitems = $this->getpath($dataset,
888 array('#', 'dataset_items', 0, '#', 'dataset_item'), array());
725ba2a0 889 foreach ($datasetitems as $datasetitem) {
890 $qo->dataset[$qo->datasetindex]->itemindex++;
79e25fb4
TH
891 $qo->dataset[$qo->datasetindex]->datasetitem[
892 $qo->dataset[$qo->datasetindex]->itemindex] = new stdClass();
893 $qo->dataset[$qo->datasetindex]->datasetitem[
894 $qo->dataset[$qo->datasetindex]->itemindex]->itemnumber =
895 $datasetitem['#']['number'][0]['#'];
896 $qo->dataset[$qo->datasetindex]->datasetitem[
897 $qo->dataset[$qo->datasetindex]->itemindex]->value =
898 $datasetitem['#']['value'][0]['#'];
cde2709a 899 }
725ba2a0 900 }
88bc20c3 901
fe5f4121 902 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 903
725ba2a0 904 return $qo;
905 }
906
ee259d0c 907 /**
49e2bba7
TH
908 * This is not a real question type. It's a dummy type used to specify the
909 * import category. The format is:
ee259d0c 910 * <question type="category">
911 * <category>tom/dick/harry</category>
912 * </question>
913 */
c7df5006 914 protected function import_category($question) {
0ff4bd08 915 $qo = new stdClass();
ee259d0c 916 $qo->qtype = 'category';
86b68520 917 $qo->category = $this->import_text($question['#']['category'][0]['#']['text']);
ee259d0c 918 return $qo;
919 }
920
c81415c7 921 /**
49e2bba7 922 * Parse the array of lines into an array of questions
c81415c7 923 * this *could* burn memory - but it won't happen that much
924 * so fingers crossed!
49e2bba7 925 * @param array of lines from the input file.
e2bb3c92 926 * @param stdClass $context
49e2bba7 927 * @return array (of objects) question objects.
c81415c7 928 */
4d188926 929 protected function readquestions($lines) {
d2fd7262 930 // We just need it as one big string.
7e252acf 931 $lines = implode('', $lines);
84769fd8 932
49e2bba7 933 // This converts xml to big nasty data structure
d2fd7262 934 // the 0 means keep white space as it is (important for markdown format).
49e2bba7 935 try {
7e252acf 936 $xml = xmlize($lines, 0, 'UTF-8', true);
79e25fb4 937 } catch (xml_format_exception $e) {
49e2bba7
TH
938 $this->error($e->getMessage(), '');
939 return false;
940 }
7e252acf 941 unset($lines); // No need to keep this in memory.
397acdfd
JP
942 return $this->import_questions($xml['quiz']['#']['question']);
943 }
7e252acf 944
397acdfd
JP
945 /**
946 * @param array $xml the xmlized xml
947 * @return stdClass[] question objects to pass to question type save_question_options
948 */
949 public function import_questions($xml) {
84769fd8 950 $questions = array();
951
d2fd7262 952 // Iterate through questions.
397acdfd
JP
953 foreach ($xml as $questionxml) {
954 $qo = $this->import_question($questionxml);
84769fd8 955
d2fd7262 956 // Stick the result in the $questions array.
84769fd8 957 if ($qo) {
958 $questions[] = $qo;
959 }
960 }
84769fd8 961 return $questions;
962 }
963
397acdfd
JP
964 /**
965 * @param array $questionxml xml describing the question
966 * @return null|stdClass an object with data to be fed to question type save_question_options
967 */
968 protected function import_question($questionxml) {
969 $questiontype = $questionxml['@']['type'];
970
971 if ($questiontype == 'multichoice') {
972 return $this->import_multichoice($questionxml);
973 } else if ($questiontype == 'truefalse') {
974 return $this->import_truefalse($questionxml);
975 } else if ($questiontype == 'shortanswer') {
976 return $this->import_shortanswer($questionxml);
977 } else if ($questiontype == 'numerical') {
978 return $this->import_numerical($questionxml);
979 } else if ($questiontype == 'description') {
980 return $this->import_description($questionxml);
981 } else if ($questiontype == 'matching' || $questiontype == 'match') {
982 return $this->import_match($questionxml);
983 } else if ($questiontype == 'cloze' || $questiontype == 'multianswer') {
984 return $this->import_multianswer($questionxml);
985 } else if ($questiontype == 'essay') {
986 return $this->import_essay($questionxml);
987 } else if ($questiontype == 'calculated') {
988 return $this->import_calculated($questionxml);
989 } else if ($questiontype == 'calculatedsimple') {
990 $qo = $this->import_calculated($questionxml);
991 $qo->qtype = 'calculatedsimple';
992 return $qo;
993 } else if ($questiontype == 'calculatedmulti') {
994 $qo = $this->import_calculated($questionxml);
995 $qo->qtype = 'calculatedmulti';
996 return $qo;
997 } else if ($questiontype == 'category') {
998 return $this->import_category($questionxml);
999
1000 } else {
1001 // Not a type we handle ourselves. See if the question type wants
1002 // to handle it.
1003 if (!$qo = $this->try_importing_using_qtypes($questionxml, null, null, $questiontype)) {
1004 $this->error(get_string('xmltypeunsupported', 'qformat_xml', $questiontype));
1005 return null;
1006 }
1007 return $qo;
1008 }
1009 }
1010
d2fd7262 1011 // EXPORT FUNCTIONS START HERE.
84769fd8 1012
c7df5006 1013 public function export_file_extension() {
46732124 1014 return '.xml';
84769fd8 1015 }
1016
c81415c7 1017 /**
77d0f5f0
TH
1018 * Turn the internal question type name into a human readable form.
1019 * (In the past, the code used to use integers internally. Now, it uses
1020 * strings, so there is less need for this, but to maintain
1021 * backwards-compatibility we change two of the type names.)
1022 * @param string $qtype question type plugin name.
1023 * @return string $qtype string to use in the file.
c81415c7 1024 */
77d0f5f0
TH
1025 protected function get_qtype($qtype) {
1026 switch($qtype) {
1027 case 'match':
49e2bba7 1028 return 'matching';
77d0f5f0 1029 case 'multianswer':
49e2bba7 1030 return 'cloze';
49e2bba7 1031 default:
77d0f5f0 1032 return $qtype;
84769fd8 1033 }
84769fd8 1034 }
1035
6e557c08 1036 /**
c81415c7 1037 * Convert internal Moodle text format code into
1038 * human readable form
6e557c08 1039 * @param int id internal code
1040 * @return string format text
c81415c7 1041 */
4a84c94b 1042 public function get_format($id) {
49e2bba7
TH
1043 switch($id) {
1044 case FORMAT_MOODLE:
1045 return 'moodle_auto_format';
1046 case FORMAT_HTML:
1047 return 'html';
1048 case FORMAT_PLAIN:
1049 return 'plain_text';
1050 case FORMAT_WIKI:
1051 return 'wiki_like';
1052 case FORMAT_MARKDOWN:
1053 return 'markdown';
1054 default:
1055 return 'unknown';
84769fd8 1056 }
84769fd8 1057 }
1058
6e557c08 1059 /**
88bc20c3 1060 * Convert internal single question code into
c81415c7 1061 * human readable form
6e557c08 1062 * @param int id single question code
1063 * @return string single question string
c81415c7 1064 */
c7df5006 1065 public function get_single($id) {
49e2bba7
TH
1066 switch($id) {
1067 case 0:
1068 return 'false';
1069 case 1:
1070 return 'true';
1071 default:
1072 return 'unknown';
84769fd8 1073 }
84769fd8 1074 }
1075
1b343d52
TH
1076 /**
1077 * Take a string, and wrap it in a CDATA secion, if that is required to make
1078 * the output XML valid.
1079 * @param string $string a string
1080 * @return string the string, wrapped in CDATA if necessary.
1081 */
1082 public function xml_escape($string) {
1083 if (!empty($string) && htmlspecialchars($string) != $string) {
46e4c35a
TH
1084 // If the string contains something that looks like the end
1085 // of a CDATA section, then we need to avoid errors by splitting
1086 // the string between two CDATA sections.
1087 $string = str_replace(']]>', ']]]]><![CDATA[>', $string);
1b343d52
TH
1088 return "<![CDATA[{$string}]]>";
1089 } else {
1090 return $string;
1091 }
1092 }
1093
6e557c08 1094 /**
49e2bba7
TH
1095 * Generates <text></text> tags, processing raw text therein
1096 * @param string $raw the content to output.
1097 * @param int $indent the current indent level.
f7970e3c 1098 * @param bool $short stick it on one line.
49e2bba7 1099 * @return string formatted text.
c81415c7 1100 */
c7df5006 1101 public function writetext($raw, $indent = 0, $short = true) {
49e2bba7 1102 $indent = str_repeat(' ', $indent);
1b343d52 1103 $raw = $this->xml_escape($raw);
84769fd8 1104
1105 if ($short) {
f4fe3968 1106 $xml = "{$indent}<text>{$raw}</text>\n";
4f290077 1107 } else {
f4fe3968 1108 $xml = "{$indent}<text>\n{$raw}\n{$indent}</text>\n";
84769fd8 1109 }
1110
1111 return $xml;
1112 }
88bc20c3 1113
7e7fdf59
TH
1114 /**
1115 * Generte the XML to represent some files.
1116 * @param array of store array of stored_file objects.
1117 * @return string $string the XML.
1118 */
1119 public function write_files($files) {
1120 if (empty($files)) {
1121 return '';
1122 }
1123 $string = '';
1124 foreach ($files as $file) {
1125 if ($file->is_directory()) {
1126 continue;
1127 }
624ff5ba 1128 $string .= '<file name="' . $file->get_filename() . '" path="' . $file->get_filepath() . '" encoding="base64">';
7e7fdf59 1129 $string .= base64_encode($file->get_content());
624ff5ba 1130 $string .= "</file>\n";
7e7fdf59
TH
1131 }
1132 return $string;
1133 }
1134
c7df5006 1135 protected function presave_process($content) {
d2fd7262 1136 // Override to allow us to add xml headers and footers.
49e2bba7
TH
1137 return '<?xml version="1.0" encoding="UTF-8"?>
1138<quiz>
1139' . $content . '</quiz>';
84769fd8 1140 }
1141
6e557c08 1142 /**
c81415c7 1143 * Turns question into an xml segment
49e2bba7 1144 * @param object $question the question data.
6e557c08 1145 * @return string xml segment
c81415c7 1146 */
c7df5006 1147 public function writequestion($question) {
d649fb02 1148 global $CFG, $OUTPUT;
cde2709a 1149
b31d6664 1150 $invalidquestion = false;
cde2709a
DC
1151 $fs = get_file_storage();
1152 $contextid = $question->contextid;
fe041243
TH
1153 // Get files used by the questiontext.
1154 $question->questiontextfiles = $fs->get_area_files(
1155 $contextid, 'question', 'questiontext', $question->id);
1156 // Get files used by the generalfeedback.
1157 $question->generalfeedbackfiles = $fs->get_area_files(
1158 $contextid, 'question', 'generalfeedback', $question->id);
49e2bba7
TH
1159 if (!empty($question->options->answers)) {
1160 foreach ($question->options->answers as $answer) {
2a6c5c52 1161 $answer->answerfiles = $fs->get_area_files(
1162 $contextid, 'question', 'answer', $answer->id);
fe041243
TH
1163 $answer->feedbackfiles = $fs->get_area_files(
1164 $contextid, 'question', 'answerfeedback', $answer->id);
49e2bba7
TH
1165 }
1166 }
1167
1168 $expout = '';
84769fd8 1169
49e2bba7 1170 // Add a comment linking this to the original question id.
f4fe3968 1171 $expout .= "<!-- question: {$question->id} -->\n";
84769fd8 1172
d2fd7262 1173 // Check question type.
77d0f5f0 1174 $questiontype = $this->get_qtype($question->qtype);
46013523 1175
77d0f5f0 1176 // Categories are a special case.
f1abd39f 1177 if ($question->qtype == 'category') {
49e2bba7 1178 $categorypath = $this->writetext($question->category);
f1abd39f 1179 $expout .= " <question type=\"category\">\n";
1180 $expout .= " <category>\n";
f4fe3968 1181 $expout .= " {$categorypath}\n";
f1abd39f 1182 $expout .= " </category>\n";
1183 $expout .= " </question>\n";
1184 return $expout;
77d0f5f0 1185 }
49e2bba7 1186
77d0f5f0
TH
1187 // Now we know we are are handing a real question.
1188 // Output the generic information.
f4fe3968 1189 $expout .= " <question type=\"{$questiontype}\">\n";
77d0f5f0
TH
1190 $expout .= " <name>\n";
1191 $expout .= $this->writetext($question->name, 3);
1192 $expout .= " </name>\n";
1193 $expout .= " <questiontext {$this->format($question->questiontextformat)}>\n";
1194 $expout .= $this->writetext($question->questiontext, 3);
7e7fdf59 1195 $expout .= $this->write_files($question->questiontextfiles);
77d0f5f0
TH
1196 $expout .= " </questiontext>\n";
1197 $expout .= " <generalfeedback {$this->format($question->generalfeedbackformat)}>\n";
1198 $expout .= $this->writetext($question->generalfeedback, 3);
7e7fdf59 1199 $expout .= $this->write_files($question->generalfeedbackfiles);
77d0f5f0
TH
1200 $expout .= " </generalfeedback>\n";
1201 if ($question->qtype != 'multianswer') {
49e2bba7 1202 $expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
7b8bc256 1203 }
77d0f5f0
TH
1204 $expout .= " <penalty>{$question->penalty}</penalty>\n";
1205 $expout .= " <hidden>{$question->hidden}</hidden>\n";
7b8bc256 1206
77d0f5f0 1207 // The rest of the output depends on question type.
84769fd8 1208 switch($question->qtype) {
79e25fb4 1209 case 'category':
d2fd7262 1210 // Not a qtype really - dummy used for category switching.
79e25fb4
TH
1211 break;
1212
1213 case 'truefalse':
1214 $trueanswer = $question->options->answers[$question->options->trueanswer];
1215 $trueanswer->answer = 'true';
1216 $expout .= $this->write_answer($trueanswer);
1217
1218 $falseanswer = $question->options->answers[$question->options->falseanswer];
1219 $falseanswer->answer = 'false';
1220 $expout .= $this->write_answer($falseanswer);
1221 break;
1222
1223 case 'multichoice':
1224 $expout .= " <single>" . $this->get_single($question->options->single) .
1225 "</single>\n";
1226 $expout .= " <shuffleanswers>" .
1227 $this->get_single($question->options->shuffleanswers) .
1228 "</shuffleanswers>\n";
1229 $expout .= " <answernumbering>" . $question->options->answernumbering .
1230 "</answernumbering>\n";
78fc17eb 1231 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
79e25fb4
TH
1232 $expout .= $this->write_answers($question->options->answers);
1233 break;
1234
1235 case 'shortanswer':
1236 $expout .= " <usecase>{$question->options->usecase}</usecase>\n";
1237 $expout .= $this->write_answers($question->options->answers);
1238 break;
1239
1240 case 'numerical':
1241 foreach ($question->options->answers as $answer) {
1242 $expout .= $this->write_answer($answer,
f4fe3968 1243 " <tolerance>{$answer->tolerance}</tolerance>\n");
84769fd8 1244 }
cde2709a 1245
cde2709a
DC
1246 $units = $question->options->units;
1247 if (count($units)) {
1248 $expout .= "<units>\n";
1249 foreach ($units as $unit) {
1250 $expout .= " <unit>\n";
1251 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1252 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1253 $expout .= " </unit>\n";
1254 }
1255 $expout .= "</units>\n";
725ba2a0 1256 }
79e25fb4
TH
1257 if (isset($question->options->unitgradingtype)) {
1258 $expout .= " <unitgradingtype>" . $question->options->unitgradingtype .
1259 "</unitgradingtype>\n";
1260 }
1261 if (isset($question->options->unitpenalty)) {
1262 $expout .= " <unitpenalty>{$question->options->unitpenalty}</unitpenalty>\n";
1263 }
1264 if (isset($question->options->showunits)) {
1265 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1266 }
1267 if (isset($question->options->unitsleft)) {
1268 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1269 }
1270 if (!empty($question->options->instructionsformat)) {
1271 $files = $fs->get_area_files($contextid, 'qtype_numerical',
1272 'instruction', $question->id);
1273 $expout .= " <instructions " .
1274 $this->format($question->options->instructionsformat) . ">\n";
1275 $expout .= $this->writetext($question->options->instructions, 3);
7e7fdf59 1276 $expout .= $this->write_files($files);
79e25fb4
TH
1277 $expout .= " </instructions>\n";
1278 }
1279 break;
1280
1281 case 'match':
1282 $expout .= " <shuffleanswers>" .
1283 $this->get_single($question->options->shuffleanswers) .
1284 "</shuffleanswers>\n";
78fc17eb 1285 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
79e25fb4
TH
1286 foreach ($question->options->subquestions as $subquestion) {
1287 $files = $fs->get_area_files($contextid, 'qtype_match',
1288 'subquestion', $subquestion->id);
1289 $expout .= " <subquestion " .
1290 $this->format($subquestion->questiontextformat) . ">\n";
1291 $expout .= $this->writetext($subquestion->questiontext, 3);
7e7fdf59 1292 $expout .= $this->write_files($files);
79e25fb4
TH
1293 $expout .= " <answer>\n";
1294 $expout .= $this->writetext($subquestion->answertext, 4);
1295 $expout .= " </answer>\n";
1296 $expout .= " </subquestion>\n";
1297 }
1298 break;
1299
1300 case 'description':
1301 // Nothing else to do.
1302 break;
1303
1304 case 'multianswer':
77d0f5f0 1305 foreach ($question->options->questions as $index => $subq) {
5c0b5ba5 1306 $expout = str_replace('{#' . $index . '}', $subq->questiontext, $expout);
79e25fb4
TH
1307 }
1308 break;
1309
1310 case 'essay':
8a5d05ee
TH
1311 $expout .= " <responseformat>" . $question->options->responseformat .
1312 "</responseformat>\n";
a4f765eb
KT
1313 $expout .= " <responserequired>" . $question->options->responserequired .
1314 "</responserequired>\n";
8a5d05ee
TH
1315 $expout .= " <responsefieldlines>" . $question->options->responsefieldlines .
1316 "</responsefieldlines>\n";
1317 $expout .= " <attachments>" . $question->options->attachments .
1318 "</attachments>\n";
a4f765eb
KT
1319 $expout .= " <attachmentsrequired>" . $question->options->attachmentsrequired .
1320 "</attachmentsrequired>\n";
8a5d05ee
TH
1321 $expout .= " <graderinfo " .
1322 $this->format($question->options->graderinfoformat) . ">\n";
1323 $expout .= $this->writetext($question->options->graderinfo, 3);
7e7fdf59 1324 $expout .= $this->write_files($fs->get_area_files($contextid, 'qtype_essay',
474abf12 1325 'graderinfo', $question->id));
8a5d05ee 1326 $expout .= " </graderinfo>\n";
60527d0c
JMV
1327 $expout .= " <responsetemplate " .
1328 $this->format($question->options->responsetemplateformat) . ">\n";
1329 $expout .= $this->writetext($question->options->responsetemplate, 3);
1330 $expout .= " </responsetemplate>\n";
79e25fb4
TH
1331 break;
1332
1333 case 'calculated':
1334 case 'calculatedsimple':
1335 case 'calculatedmulti':
1336 $expout .= " <synchronize>{$question->options->synchronize}</synchronize>\n";
1337 $expout .= " <single>{$question->options->single}</single>\n";
1338 $expout .= " <answernumbering>" . $question->options->answernumbering .
1339 "</answernumbering>\n";
1f1c60c6 1340 $expout .= " <shuffleanswers>" . $question->options->shuffleanswers .
79e25fb4
TH
1341 "</shuffleanswers>\n";
1342
1343 $component = 'qtype_' . $question->qtype;
1344 $files = $fs->get_area_files($contextid, $component,
1345 'correctfeedback', $question->id);
1346 $expout .= " <correctfeedback>\n";
1347 $expout .= $this->writetext($question->options->correctfeedback, 3);
7e7fdf59 1348 $expout .= $this->write_files($files);
79e25fb4
TH
1349 $expout .= " </correctfeedback>\n";
1350
1351 $files = $fs->get_area_files($contextid, $component,
1352 'partiallycorrectfeedback', $question->id);
1353 $expout .= " <partiallycorrectfeedback>\n";
1354 $expout .= $this->writetext($question->options->partiallycorrectfeedback, 3);
7e7fdf59 1355 $expout .= $this->write_files($files);
79e25fb4
TH
1356 $expout .= " </partiallycorrectfeedback>\n";
1357
1358 $files = $fs->get_area_files($contextid, $component,
1359 'incorrectfeedback', $question->id);
1360 $expout .= " <incorrectfeedback>\n";
1361 $expout .= $this->writetext($question->options->incorrectfeedback, 3);
7e7fdf59 1362 $expout .= $this->write_files($files);
79e25fb4
TH
1363 $expout .= " </incorrectfeedback>\n";
1364
1365 foreach ($question->options->answers as $answer) {
1366 $percent = 100 * $answer->fraction;
f4fe3968 1367 $expout .= "<answer fraction=\"{$percent}\">\n";
d2fd7262 1368 // The "<text/>" tags are an added feature, old files won't have them.
79e25fb4
TH
1369 $expout .= " <text>{$answer->answer}</text>\n";
1370 $expout .= " <tolerance>{$answer->tolerance}</tolerance>\n";
1371 $expout .= " <tolerancetype>{$answer->tolerancetype}</tolerancetype>\n";
1372 $expout .= " <correctanswerformat>" .
1373 $answer->correctanswerformat . "</correctanswerformat>\n";
1374 $expout .= " <correctanswerlength>" .
1375 $answer->correctanswerlength . "</correctanswerlength>\n";
1376 $expout .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1377 $files = $fs->get_area_files($contextid, $component,
1378 'instruction', $question->id);
1379 $expout .= $this->writetext($answer->feedback);
7e7fdf59 1380 $expout .= $this->write_files($answer->feedbackfiles);
79e25fb4
TH
1381 $expout .= " </feedback>\n";
1382 $expout .= "</answer>\n";
1383 }
1384 if (isset($question->options->unitgradingtype)) {
1385 $expout .= " <unitgradingtype>" .
1386 $question->options->unitgradingtype . "</unitgradingtype>\n";
1387 }
1388 if (isset($question->options->unitpenalty)) {
1389 $expout .= " <unitpenalty>" .
1390 $question->options->unitpenalty . "</unitpenalty>\n";
1391 }
1392 if (isset($question->options->showunits)) {
1393 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1394 }
1395 if (isset($question->options->unitsleft)) {
1396 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1397 }
d649fb02 1398
79e25fb4
TH
1399 if (isset($question->options->instructionsformat)) {
1400 $files = $fs->get_area_files($contextid, $component,
1401 'instruction', $question->id);
1402 $expout .= " <instructions " .
1403 $this->format($question->options->instructionsformat) . ">\n";
1404 $expout .= $this->writetext($question->options->instructions, 3);
7e7fdf59 1405 $expout .= $this->write_files($files);
79e25fb4
TH
1406 $expout .= " </instructions>\n";
1407 }
1408
1409 if (isset($question->options->units)) {
1410 $units = $question->options->units;
1411 if (count($units)) {
1412 $expout .= "<units>\n";
1413 foreach ($units as $unit) {
1414 $expout .= " <unit>\n";
1415 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1416 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1417 $expout .= " </unit>\n";
1418 }
1419 $expout .= "</units>\n";
9c1c6c7f 1420 }
79e25fb4
TH
1421 }
1422
1423 // The tag $question->export_process has been set so we get all the
1424 // data items in the database from the function
1425 // qtype_calculated::get_question_options calculatedsimple defaults
d2fd7262 1426 // to calculated.
79e25fb4
TH
1427 if (isset($question->options->datasets) && count($question->options->datasets)) {
1428 $expout .= "<dataset_definitions>\n";
1429 foreach ($question->options->datasets as $def) {
1430 $expout .= "<dataset_definition>\n";
1431 $expout .= " <status>".$this->writetext($def->status)."</status>\n";
1432 $expout .= " <name>".$this->writetext($def->name)."</name>\n";
e231a3ff 1433 if ($question->qtype == 'calculated') {
79e25fb4
TH
1434 $expout .= " <type>calculated</type>\n";
1435 } else {
1436 $expout .= " <type>calculatedsimple</type>\n";
1437 }
1438 $expout .= " <distribution>" . $this->writetext($def->distribution) .
1439 "</distribution>\n";
1440 $expout .= " <minimum>" . $this->writetext($def->minimum) .
1441 "</minimum>\n";
1442 $expout .= " <maximum>" . $this->writetext($def->maximum) .
1443 "</maximum>\n";
1444 $expout .= " <decimals>" . $this->writetext($def->decimals) .
1445 "</decimals>\n";
f4fe3968 1446 $expout .= " <itemcount>{$def->itemcount}</itemcount>\n";
79e25fb4
TH
1447 if ($def->itemcount > 0) {
1448 $expout .= " <dataset_items>\n";
1449 foreach ($def->items as $item) {
1450 $expout .= " <dataset_item>\n";
1451 $expout .= " <number>".$item->itemnumber."</number>\n";
1452 $expout .= " <value>".$item->value."</value>\n";
1453 $expout .= " </dataset_item>\n";
1454 }
1455 $expout .= " </dataset_items>\n";
1456 $expout .= " <number_of_items>" . $def->number_of_items .
1457 "</number_of_items>\n";
88bc20c3 1458 }
79e25fb4
TH
1459 $expout .= "</dataset_definition>\n";
1460 }
1461 $expout .= "</dataset_definitions>\n";
88bc20c3 1462 }
79e25fb4 1463 break;
49e2bba7 1464
79e25fb4 1465 default:
b31d6664 1466 // Try support by optional plugin.
79e25fb4 1467 if (!$data = $this->try_exporting_using_qtypes($question->qtype, $question)) {
b31d6664
JMV
1468 $invalidquestion = true;
1469 } else {
1470 $expout .= $data;
79e25fb4 1471 }
84769fd8 1472 }
1473
49e2bba7
TH
1474 // Output any hints.
1475 $expout .= $this->write_hints($question);
1476
4f290077 1477 // Write the question tags.
3bf5fcf2 1478 if (core_tag_tag::is_enabled('core_question', 'question')) {
1796eb18 1479 $tagobjects = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
3bf5fcf2
SL
1480
1481 if (!is_null($tagobjects)) {
1796eb18
SL
1482 $context = context::instance_by_id($contextid);
1483 $sortedtagobjects = question_sort_tags($tagobjects, $context, [$this->course]);
3bf5fcf2
SL
1484
1485 if (!empty($sortedtagobjects->coursetags)) {
1486 // Set them on the form to be rendered as existing tags.
1487 $expout .= " <coursetags>\n";
1488 foreach ($sortedtagobjects->coursetags as $coursetag) {
1489 $expout .= " <tag>" . $this->writetext($coursetag, 0, true) . "</tag>\n";
1490 }
1491 $expout .= " </coursetags>\n";
3a7005c9 1492 }
3a7005c9 1493
3bf5fcf2
SL
1494 if (!empty($sortedtagobjects->tags)) {
1495 $expout .= " <tags>\n";
1496 foreach ($sortedtagobjects->tags as $tag) {
1497 $expout .= " <tag>" . $this->writetext($tag, 0, true) . "</tag>\n";
1498 }
1499 $expout .= " </tags>\n";
1500 }
4f290077
TH
1501 }
1502 }
1503
d2fd7262 1504 // Close the question tag.
49e2bba7 1505 $expout .= " </question>\n";
b31d6664
JMV
1506 if ($invalidquestion) {
1507 return '';
1508 } else {
1509 return $expout;
1510 }
84769fd8 1511 }
49e2bba7
TH
1512
1513 public function write_answers($answers) {
1514 if (empty($answers)) {
1515 return;
1516 }
1517 $output = '';
1518 foreach ($answers as $answer) {
1519 $output .= $this->write_answer($answer);
1520 }
1521 return $output;
1522 }
1523
1524 public function write_answer($answer, $extra = '') {
1525 $percent = $answer->fraction * 100;
1526 $output = '';
f4fe3968 1527 $output .= " <answer fraction=\"{$percent}\" {$this->format($answer->answerformat)}>\n";
49e2bba7 1528 $output .= $this->writetext($answer->answer, 3);
7e7fdf59 1529 $output .= $this->write_files($answer->answerfiles);
5f7cfba7 1530 $output .= " <feedback {$this->format($answer->feedbackformat)}>\n";
49e2bba7 1531 $output .= $this->writetext($answer->feedback, 4);
7e7fdf59 1532 $output .= $this->write_files($answer->feedbackfiles);
49e2bba7
TH
1533 $output .= " </feedback>\n";
1534 $output .= $extra;
1535 $output .= " </answer>\n";
1536 return $output;
1537 }
1538
78fc17eb
TH
1539 /**
1540 * Write out the hints.
1541 * @param object $question the question definition data.
1542 * @return string XML to output.
1543 */
49e2bba7
TH
1544 public function write_hints($question) {
1545 if (empty($question->hints)) {
1546 return '';
1547 }
1548
1549 $output = '';
1550 foreach ($question->hints as $hint) {
78fc17eb 1551 $output .= $this->write_hint($hint, $question->contextid);
49e2bba7
TH
1552 }
1553 return $output;
1554 }
1555
5f7cfba7 1556 /**
2ecbad41 1557 * @param int $format a FORMAT_... constant.
5f7cfba7
TH
1558 * @return string the attribute to add to an XML tag.
1559 */
2ecbad41 1560 public function format($format) {
5f7cfba7
TH
1561 return 'format="' . $this->get_format($format) . '"';
1562 }
1563
78fc17eb
TH
1564 public function write_hint($hint, $contextid) {
1565 $fs = get_file_storage();
1566 $files = $fs->get_area_files($contextid, 'question', 'hint', $hint->id);
1567
49e2bba7 1568 $output = '';
5f7cfba7 1569 $output .= " <hint {$this->format($hint->hintformat)}>\n";
49e2bba7 1570 $output .= ' ' . $this->writetext($hint->hint);
78fc17eb 1571
49e2bba7
TH
1572 if (!empty($hint->shownumcorrect)) {
1573 $output .= " <shownumcorrect/>\n";
1574 }
1575 if (!empty($hint->clearwrong)) {
1576 $output .= " <clearwrong/>\n";
1577 }
78fc17eb 1578
49e2bba7 1579 if (!empty($hint->options)) {
1b343d52 1580 $output .= ' <options>' . $this->xml_escape($hint->options) . "</options>\n";
49e2bba7 1581 }
7e7fdf59 1582 $output .= $this->write_files($files);
49e2bba7
TH
1583 $output .= " </hint>\n";
1584 return $output;
1585 }
1586
78fc17eb
TH
1587 /**
1588 * Output the combined feedback fields.
1589 * @param object $questionoptions the question definition data.
1590 * @param int $questionid the question id.
1591 * @param int $contextid the question context id.
1592 * @return string XML to output.
1593 */
1594 public function write_combined_feedback($questionoptions, $questionid, $contextid) {
1595 $fs = get_file_storage();
1596 $output = '';
1597
1598 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
1599 foreach ($fields as $field) {
1600 $formatfield = $field . 'format';
1601 $files = $fs->get_area_files($contextid, 'question', $field, $questionid);
1602
1603 $output .= " <{$field} {$this->format($questionoptions->$formatfield)}>\n";
1604 $output .= ' ' . $this->writetext($questionoptions->$field);
7e7fdf59 1605 $output .= $this->write_files($files);
78fc17eb
TH
1606 $output .= " </{$field}>\n";
1607 }
1608
49e2bba7
TH
1609 if (!empty($questionoptions->shownumcorrect)) {
1610 $output .= " <shownumcorrect/>\n";
1611 }
1612 return $output;
1613 }
84769fd8 1614}