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