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