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