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