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