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