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