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