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