84769fd8 |
1 | <?php // $Id$ |
2 | // |
3 | /////////////////////////////////////////////////////////////// |
4 | // XML import/export |
5 | // |
6 | ////////////////////////////////////////////////////////////////////////// |
7 | // Based on default.php, included by ../import.php |
41a89a07 |
8 | /** |
9 | * @package questionbank |
10 | * @subpackage importexport |
11 | */ |
84769fd8 |
12 | require_once( "$CFG->libdir/xmlize.php" ); |
13 | |
f5565b69 |
14 | class qformat_xml extends qformat_default { |
84769fd8 |
15 | |
16 | function provide_import() { |
17 | return true; |
18 | } |
19 | |
20 | function provide_export() { |
21 | return true; |
22 | } |
23 | |
24 | // IMPORT FUNCTIONS START HERE |
25 | |
6e557c08 |
26 | /** |
c81415c7 |
27 | * Translate human readable format name |
28 | * into internal Moodle code number |
6e557c08 |
29 | * @param string name format name from xml file |
30 | * @return int Moodle format code |
c81415c7 |
31 | */ |
84769fd8 |
32 | function trans_format( $name ) { |
84769fd8 |
33 | $name = trim($name); |
34 | |
35 | if ($name=='moodle_auto_format') { |
36 | $id = 0; |
37 | } |
38 | elseif ($name=='html') { |
39 | $id = 1; |
40 | } |
41 | elseif ($name=='plain_text') { |
42 | $id = 2; |
43 | } |
44 | elseif ($name=='wiki_like') { |
45 | $id = 3; |
46 | } |
47 | elseif ($name=='markdown') { |
48 | $id = 4; |
49 | } |
50 | else { |
51 | $id = 0; // or maybe warning required |
52 | } |
53 | return $id; |
54 | } |
55 | |
6e557c08 |
56 | /** |
c81415c7 |
57 | * Translate human readable single answer option |
58 | * to internal code number |
6e557c08 |
59 | * @param string name true/false |
60 | * @return int internal code number |
c81415c7 |
61 | */ |
84769fd8 |
62 | function trans_single( $name ) { |
2da44816 |
63 | $name = trim($name); |
64 | if ($name == "false" || !$name) { |
65 | return 0; |
66 | } else { |
67 | return 1; |
68 | } |
84769fd8 |
69 | } |
70 | |
6e557c08 |
71 | /** |
c81415c7 |
72 | * process text string from xml file |
6e557c08 |
73 | * @param array $text bit of xml tree after ['text'] |
74 | * @return string processed text |
c81415c7 |
75 | */ |
84769fd8 |
76 | function import_text( $text ) { |
17102269 |
77 | // quick sanity check |
78 | if (empty($text)) { |
79 | return ''; |
80 | } |
84769fd8 |
81 | $data = $text[0]['#']; |
84769fd8 |
82 | return addslashes(trim( $data )); |
83 | } |
84 | |
307f045f |
85 | /** |
86 | * Process text from an element in the XML that may or not be there. |
87 | * @param string $subelement the name of the element which is either present or missing. |
88 | * @param array $question a bit of xml tree, this method looks for $question['#'][$subelement][0]['#']['text']. |
89 | * @return string If $subelement is present, return the content of the text tag inside it. |
90 | * Otherwise returns an empty string. |
91 | */ |
92 | function import_optional_text($subelement, $question) { |
93 | if (array_key_exists($subelement, $question['#'])) { |
94 | return $this->import_text($question['#'][$subelement][0]['#']['text']); |
95 | } else { |
96 | return ''; |
97 | } |
98 | } |
99 | |
46013523 |
100 | /** |
101 | * return the value of a node, given a path to the node |
102 | * if it doesn't exist return the default value |
103 | * @param array xml data to read |
104 | * @param array path path to node expressed as array |
105 | * @param mixed default |
106 | * @param bool istext process as text |
107 | * @param string error if set value must exist, return false and issue message if not |
108 | * @return mixed value |
109 | */ |
110 | function getpath( $xml, $path, $default, $istext=false, $error='' ) { |
111 | foreach ($path as $index) { |
112 | if (empty($xml[$index])) { |
113 | if (!empty($error)) { |
114 | $this->error( $error ); |
115 | return false; |
116 | } else { |
117 | return $default; |
118 | } |
119 | } |
120 | else $xml = $xml[$index]; |
121 | } |
122 | if ($istext) { |
123 | $xml = addslashes( trim( $xml ) ); |
124 | } |
125 | |
126 | return $xml; |
127 | } |
128 | |
129 | |
6e557c08 |
130 | /** |
c81415c7 |
131 | * import parts of question common to all types |
6e557c08 |
132 | * @param array question question array from xml tree |
133 | * @return object question object |
c81415c7 |
134 | */ |
84769fd8 |
135 | function import_headers( $question ) { |
46013523 |
136 | // get some error strings |
137 | $error_noname = get_string( 'xmlimportnoname','quiz' ); |
138 | $error_noquestion = get_string( 'xmlimportnoquestion','quiz' ); |
139 | |
84769fd8 |
140 | // this routine initialises the question object |
5bed54e1 |
141 | $qo = $this->defaultquestion(); |
84769fd8 |
142 | |
46013523 |
143 | // question name |
144 | $qo->name = $this->getpath( $question, array('#','name',0,'#','text',0,'#'), '', true, $error_noname ); |
6af98025 |
145 | $qo->questiontext = $this->getpath( $question, array('#','questiontext',0,'#','text',0,'#'), '', true ); |
46013523 |
146 | $qo->questiontextformat = $this->getpath( $question, array('#','questiontext',0,'@','format'), '' ); |
147 | $image = $this->getpath( $question, array('#','image',0,'#'), $qo->image ); |
148 | $image_base64 = $this->getpath( $question, array('#','image_base64','0','#'),'' ); |
149 | if (!empty($image_base64)) { |
150 | $qo->image = $this->importimagefile( $image, stripslashes(image_base64) ); |
151 | } |
152 | $qo->generalfeedback = $this->getpath( $question, array('#','generalfeedback',0,'#','text',0,'#'), $qo->generalfeedback, true ); |
153 | $qo->defaultgrade = $this->getpath( $question, array('#','defaultgrade',0,'#'), $qo->defaultgrade ); |
154 | $qo->penalty = $this->getpath( $question, array('#','penalty',0,'#'), $qo->penalty ); |
84769fd8 |
155 | |
156 | return $qo; |
157 | } |
158 | |
6e557c08 |
159 | /** |
c81415c7 |
160 | * import the common parts of a single answer |
6e557c08 |
161 | * @param array answer xml tree for single answer |
162 | * @return object answer object |
c81415c7 |
163 | */ |
84769fd8 |
164 | function import_answer( $answer ) { |
6af98025 |
165 | $fraction = $this->getpath($answer, array('@','fraction'), 0); |
166 | $text = $this->getpath( $answer, array('#','text',0,'#'),'',true); |
84769fd8 |
167 | $feedback = $this->import_text( $answer['#']['feedback'][0]['#']['text'] ); |
168 | |
169 | $ans = null; |
170 | $ans->answer = $text; |
171 | $ans->fraction = $fraction / 100; |
172 | $ans->feedback = $feedback; |
173 | |
174 | return $ans; |
175 | } |
176 | |
6e557c08 |
177 | /** |
c81415c7 |
178 | * import multiple choice question |
6e557c08 |
179 | * @param array question question array from xml tree |
180 | * @return object question object |
c81415c7 |
181 | */ |
84769fd8 |
182 | function import_multichoice( $question ) { |
84769fd8 |
183 | // get common parts |
184 | $qo = $this->import_headers( $question ); |
185 | |
186 | // 'header' parts particular to multichoice |
187 | $qo->qtype = MULTICHOICE; |
188 | $single = $question['#']['single'][0]['#']; |
189 | $qo->single = $this->trans_single( $single ); |
307f045f |
190 | if (array_key_exists('shuffleanswers', $question['#'])) { |
191 | $shuffleanswers = $question['#']['shuffleanswers'][0]['#']; |
192 | } else { |
193 | $shuffleanswers = 'false'; |
194 | } |
2da44816 |
195 | $qo->shuffleanswers = $this->trans_single($shuffleanswers); |
307f045f |
196 | $qo->correctfeedback = $this->import_optional_text('correctfeedback', $question); |
197 | $qo->partiallycorrectfeedback = $this->import_optional_text('partiallycorrectfeedback', $question); |
198 | $qo->incorrectfeedback = $this->import_optional_text('incorrectfeedback', $question); |
199 | |
84769fd8 |
200 | // run through the answers |
201 | $answers = $question['#']['answer']; |
202 | $a_count = 0; |
203 | foreach ($answers as $answer) { |
204 | $ans = $this->import_answer( $answer ); |
205 | $qo->answer[$a_count] = $ans->answer; |
206 | $qo->fraction[$a_count] = $ans->fraction; |
207 | $qo->feedback[$a_count] = $ans->feedback; |
208 | ++$a_count; |
209 | } |
210 | |
211 | return $qo; |
212 | } |
213 | |
6e557c08 |
214 | /** |
c81415c7 |
215 | * import cloze type question |
6e557c08 |
216 | * @param array question question array from xml tree |
217 | * @return object question object |
c81415c7 |
218 | */ |
7b8bc256 |
219 | function import_multianswer( $questions ) { |
0c6b4d2e |
220 | $questiontext = $questions['#']['questiontext'][0]['#']['text']; |
221 | $qo = qtype_multianswer_extract_question($this->import_text($questiontext)); |
7b8bc256 |
222 | |
223 | // 'header' parts particular to multianswer |
224 | $qo->qtype = MULTIANSWER; |
225 | $qo->course = $this->course; |
226 | |
71ffbac2 |
227 | if (!empty($questions)) { |
7b8bc256 |
228 | $qo->name = $this->import_text( $questions['#']['name'][0]['#']['text'] ); |
229 | } |
230 | |
231 | return $qo; |
232 | } |
233 | |
6e557c08 |
234 | /** |
c81415c7 |
235 | * import true/false type question |
6e557c08 |
236 | * @param array question question array from xml tree |
237 | * @return object question object |
c81415c7 |
238 | */ |
84769fd8 |
239 | function import_truefalse( $question ) { |
84769fd8 |
240 | // get common parts |
241 | $qo = $this->import_headers( $question ); |
242 | |
243 | // 'header' parts particular to true/false |
244 | $qo->qtype = TRUEFALSE; |
245 | |
246 | // get answer info |
3246ed33 |
247 | // |
248 | // In the past, it used to be assumed that the two answers were in the file |
249 | // true first, then false. Howevever that was not always true. Now, we |
250 | // try to match on the answer text, but in old exports, this will be a localised |
251 | // string, so if we don't find true or false, we fall back to the old system. |
252 | $first = true; |
253 | $warning = false; |
254 | foreach ($question['#']['answer'] as $answer) { |
255 | $answertext = $this->import_text($answer['#']['text']); |
256 | $feedback = $this->import_text($answer['#']['feedback'][0]['#']['text']); |
257 | if ($answertext != 'true' && $answertext != 'false') { |
258 | $warning = true; |
259 | $answertext = $first ? 'true' : 'false'; // Old style file, assume order is true/false. |
260 | } |
261 | if ($answertext == 'true') { |
262 | $qo->answer = ($answer['@']['fraction'] == 100); |
7939a4a0 |
263 | $qo->correctanswer = $qo->answer; |
3246ed33 |
264 | $qo->feedbacktrue = $feedback; |
265 | } else { |
266 | $qo->answer = ($answer['@']['fraction'] != 100); |
7939a4a0 |
267 | $qo->correctanswer = $qo->answer; |
3246ed33 |
268 | $qo->feedbackfalse = $feedback; |
269 | } |
270 | $first = false; |
84769fd8 |
271 | } |
3246ed33 |
272 | |
273 | if ($warning) { |
274 | $a = new stdClass; |
55c54868 |
275 | $a->questiontext = $qo->questiontext; |
3246ed33 |
276 | $a->answer = get_string($qo->answer ? 'true' : 'false', 'quiz'); |
277 | notify(get_string('truefalseimporterror', 'quiz', $a)); |
84769fd8 |
278 | } |
279 | |
280 | return $qo; |
281 | } |
282 | |
6e557c08 |
283 | /** |
c81415c7 |
284 | * import short answer type question |
6e557c08 |
285 | * @param array question question array from xml tree |
286 | * @return object question object |
c81415c7 |
287 | */ |
84769fd8 |
288 | function import_shortanswer( $question ) { |
84769fd8 |
289 | // get common parts |
290 | $qo = $this->import_headers( $question ); |
291 | |
292 | // header parts particular to shortanswer |
293 | $qo->qtype = SHORTANSWER; |
294 | |
295 | // get usecase |
46013523 |
296 | $qo->usecase = $this->getpath($question, array('#','usecase',0,'#'), $qo->usecase ); |
84769fd8 |
297 | |
298 | // run through the answers |
299 | $answers = $question['#']['answer']; |
300 | $a_count = 0; |
301 | foreach ($answers as $answer) { |
302 | $ans = $this->import_answer( $answer ); |
303 | $qo->answer[$a_count] = $ans->answer; |
304 | $qo->fraction[$a_count] = $ans->fraction; |
305 | $qo->feedback[$a_count] = $ans->feedback; |
306 | ++$a_count; |
307 | } |
308 | |
309 | return $qo; |
310 | } |
7b8bc256 |
311 | |
6e557c08 |
312 | /** |
c81415c7 |
313 | * import regexp type question |
6e557c08 |
314 | * @param array question question array from xml tree |
315 | * @return object question object |
c81415c7 |
316 | */ |
7b8bc256 |
317 | function import_regexp( $question ) { |
7b8bc256 |
318 | // get common parts |
319 | $qo = $this->import_headers( $question ); |
320 | |
321 | // header parts particular to shortanswer |
322 | $qo->qtype = regexp; |
323 | |
324 | // get usecase |
325 | $qo->usecase = $question['#']['usecase'][0]['#']; |
326 | |
327 | // run through the answers |
328 | $answers = $question['#']['answer']; |
329 | $a_count = 0; |
330 | foreach ($answers as $answer) { |
331 | $ans = $this->import_answer( $answer ); |
332 | $qo->answer[$a_count] = $ans->answer; |
333 | $qo->fraction[$a_count] = $ans->fraction; |
334 | $qo->feedback[$a_count] = $ans->feedback; |
335 | ++$a_count; |
336 | } |
84769fd8 |
337 | |
7b8bc256 |
338 | return $qo; |
339 | } |
340 | |
6e557c08 |
341 | /** |
c81415c7 |
342 | * import description type question |
6e557c08 |
343 | * @param array question question array from xml tree |
344 | * @return object question object |
c81415c7 |
345 | */ |
7b8bc256 |
346 | function import_description( $question ) { |
347 | // get common parts |
348 | $qo = $this->import_headers( $question ); |
349 | // header parts particular to shortanswer |
350 | $qo->qtype = DESCRIPTION; |
351 | return $qo; |
352 | } |
84769fd8 |
353 | |
6e557c08 |
354 | /** |
c81415c7 |
355 | * import numerical type question |
6e557c08 |
356 | * @param array question question array from xml tree |
357 | * @return object question object |
c81415c7 |
358 | */ |
359 | function import_numerical( $question ) { |
84769fd8 |
360 | // get common parts |
361 | $qo = $this->import_headers( $question ); |
362 | |
363 | // header parts particular to numerical |
364 | $qo->qtype = NUMERICAL; |
365 | |
366 | // get answers array |
367 | $answers = $question['#']['answer']; |
368 | $qo->answer = array(); |
369 | $qo->feedback = array(); |
370 | $qo->fraction = array(); |
371 | $qo->tolerance = array(); |
372 | foreach ($answers as $answer) { |
55c54868 |
373 | // answer outside of <text> is deprecated |
374 | if (!empty( $answer['#']['text'] )) { |
375 | $answertext = $this->import_text( $answer['#']['text'] ); |
376 | } |
377 | else { |
378 | $answertext = trim($answer['#'][0]); |
379 | } |
55894a42 |
380 | if ($answertext == '') { |
381 | $qo->answer[] = '*'; |
382 | } else { |
383 | $qo->answer[] = $answertext; |
384 | } |
a0d187bf |
385 | $qo->feedback[] = $this->import_text( $answer['#']['feedback'][0]['#']['text'] ); |
84769fd8 |
386 | $qo->tolerance[] = $answer['#']['tolerance'][0]['#']; |
55c54868 |
387 | |
388 | // fraction as a tag is deprecated |
389 | if (!empty($answer['#']['fraction'][0]['#'])) { |
390 | $qo->fraction[] = $answer['#']['fraction'][0]['#']; |
391 | } |
392 | else { |
393 | $qo->fraction[] = $answer['@']['fraction'] / 100; |
394 | } |
84769fd8 |
395 | } |
396 | |
397 | // get units array |
84769fd8 |
398 | $qo->unit = array(); |
a0d187bf |
399 | if (isset($question['#']['units'][0]['#']['unit'])) { |
400 | $units = $question['#']['units'][0]['#']['unit']; |
401 | $qo->multiplier = array(); |
402 | foreach ($units as $unit) { |
403 | $qo->multiplier[] = $unit['#']['multiplier'][0]['#']; |
404 | $qo->unit[] = $unit['#']['unit_name'][0]['#']; |
405 | } |
84769fd8 |
406 | } |
84769fd8 |
407 | return $qo; |
408 | } |
409 | |
6e557c08 |
410 | /** |
c81415c7 |
411 | * import matching type question |
6e557c08 |
412 | * @param array question question array from xml tree |
413 | * @return object question object |
c81415c7 |
414 | */ |
51bcdf28 |
415 | function import_matching( $question ) { |
51bcdf28 |
416 | // get common parts |
417 | $qo = $this->import_headers( $question ); |
418 | |
419 | // header parts particular to matching |
420 | $qo->qtype = MATCH; |
ee800653 |
421 | if (!empty($question['#']['shuffleanswers'])) { |
422 | $qo->shuffleanswers = $question['#']['shuffleanswers'][0]['#']; |
423 | } else { |
424 | $qo->shuffleanswers = false; |
425 | } |
51bcdf28 |
426 | |
427 | // get subquestions |
428 | $subquestions = $question['#']['subquestion']; |
429 | $qo->subquestions = array(); |
430 | $qo->subanswers = array(); |
431 | |
432 | // run through subquestions |
433 | foreach ($subquestions as $subquestion) { |
a0d187bf |
434 | $qtext = $this->import_text( $subquestion['#']['text'] ); |
435 | $atext = $this->import_text( $subquestion['#']['answer'][0]['#']['text'] ); |
51bcdf28 |
436 | $qo->subquestions[] = $qtext; |
437 | $qo->subanswers[] = $atext; |
438 | } |
51bcdf28 |
439 | return $qo; |
440 | } |
441 | |
6e557c08 |
442 | /** |
c81415c7 |
443 | * import essay type question |
6e557c08 |
444 | * @param array question question array from xml tree |
445 | * @return object question object |
c81415c7 |
446 | */ |
447 | function import_essay( $question ) { |
448 | // get common parts |
449 | $qo = $this->import_headers( $question ); |
450 | |
451 | // header parts particular to essay |
452 | $qo->qtype = ESSAY; |
453 | |
454 | // get feedback |
455 | $qo->feedback = $this->import_text( $question['#']['answer'][0]['#']['feedback'][0]['#']['text'] ); |
456 | |
55c54868 |
457 | // handle answer |
458 | $answer = $question['#']['answer'][0]; |
459 | |
460 | // get fraction - <fraction> tag is deprecated |
6af98025 |
461 | $qo->fraction = $this->getpath( $question, array('@','fraction'), 0 ) / 100; |
462 | $q0->fraction = $this->getpath( $question, array('#','fraction',0,'#'), $qo->fraction ); |
c81415c7 |
463 | |
464 | return $qo; |
465 | } |
84769fd8 |
466 | |
725ba2a0 |
467 | function import_calculated( $question ) { |
468 | // import numerical question |
469 | |
470 | // get common parts |
471 | $qo = $this->import_headers( $question ); |
472 | |
473 | // header parts particular to numerical |
474 | $qo->qtype = CALCULATED ;//CALCULATED; |
475 | |
476 | // get answers array |
477 | // echo "<pre> question";print_r($question);echo "</pre>"; |
478 | $answers = $question['#']['answer']; |
479 | $qo->answers = array(); |
480 | $qo->feedback = array(); |
481 | $qo->fraction = array(); |
482 | $qo->tolerance = array(); |
483 | $qo->tolerancetype = array(); |
484 | $qo->correctanswerformat = array(); |
485 | $qo->correctanswerlength = array(); |
486 | $qo->feedback = array(); |
487 | foreach ($answers as $answer) { |
488 | // answer outside of <text> is deprecated |
489 | if (!empty( $answer['#']['text'] )) { |
490 | $answertext = $this->import_text( $answer['#']['text'] ); |
491 | } |
492 | else { |
493 | $answertext = trim($answer['#'][0]); |
494 | } |
495 | if ($answertext == '') { |
496 | $qo->answers[] = '*'; |
497 | } else { |
498 | $qo->answers[] = $answertext; |
499 | } |
500 | $qo->feedback[] = $this->import_text( $answer['#']['feedback'][0]['#']['text'] ); |
501 | $qo->tolerance[] = $answer['#']['tolerance'][0]['#']; |
502 | // fraction as a tag is deprecated |
503 | if (!empty($answer['#']['fraction'][0]['#'])) { |
504 | $qo->fraction[] = $answer['#']['fraction'][0]['#']; |
505 | } |
506 | else { |
507 | $qo->fraction[] = $answer['@']['fraction'] / 100; |
508 | } |
509 | $qo->tolerancetype[] = $answer['#']['tolerancetype'][0]['#']; |
510 | $qo->correctanswerformat[] = $answer['#']['correctanswerformat'][0]['#']; |
511 | $qo->correctanswerlength[] = $answer['#']['correctanswerlength'][0]['#']; |
512 | } |
513 | // get units array |
514 | $qo->unit = array(); |
515 | if (isset($question['#']['units'][0]['#']['unit'])) { |
516 | $units = $question['#']['units'][0]['#']['unit']; |
517 | $qo->multiplier = array(); |
518 | foreach ($units as $unit) { |
519 | $qo->multiplier[] = $unit['#']['multiplier'][0]['#']; |
520 | $qo->unit[] = $unit['#']['unit_name'][0]['#']; |
521 | } |
522 | } |
523 | $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition']; |
524 | $qo->dataset = array(); |
525 | $qo->datasetindex= 0 ; |
526 | foreach ($datasets as $dataset) { |
527 | $qo->datasetindex++; |
528 | $qo->dataset[$qo->datasetindex] = new stdClass(); |
529 | $qo->dataset[$qo->datasetindex]->status = $this->import_text( $dataset['#']['status'][0]['#']['text']); |
530 | $qo->dataset[$qo->datasetindex]->name = $this->import_text( $dataset['#']['name'][0]['#']['text']); |
531 | $qo->dataset[$qo->datasetindex]->type = $dataset['#']['type'][0]['#']; |
532 | $qo->dataset[$qo->datasetindex]->distribution = $this->import_text( $dataset['#']['distribution'][0]['#']['text']); |
533 | $qo->dataset[$qo->datasetindex]->max = $this->import_text( $dataset['#']['maximum'][0]['#']['text']); |
534 | $qo->dataset[$qo->datasetindex]->min = $this->import_text( $dataset['#']['minimum'][0]['#']['text']); |
535 | $qo->dataset[$qo->datasetindex]->length = $this->import_text( $dataset['#']['decimals'][0]['#']['text']); |
536 | $qo->dataset[$qo->datasetindex]->distribution = $this->import_text( $dataset['#']['distribution'][0]['#']['text']); |
537 | $qo->dataset[$qo->datasetindex]->itemcount = $dataset['#']['itemcount'][0]['#']; |
538 | $qo->dataset[$qo->datasetindex]->datasetitem = array(); |
539 | $qo->dataset[$qo->datasetindex]->itemindex = 0; |
540 | $qo->dataset[$qo->datasetindex]->number_of_items=$dataset['#']['number_of_items'][0]['#']; |
541 | $datasetitems = $dataset['#']['dataset_items'][0]['#']['dataset_item']; |
542 | foreach ($datasetitems as $datasetitem) { |
543 | $qo->dataset[$qo->datasetindex]->itemindex++; |
544 | $qo->dataset[$qo->datasetindex]->datasetitem[$qo->dataset[$qo->datasetindex]->itemindex] = new stdClass(); |
545 | $qo->dataset[$qo->datasetindex]->datasetitem[$qo->dataset[$qo->datasetindex]->itemindex]->itemnumber = $datasetitem['#']['number'][0]['#']; //[0]['#']['number'][0]['#'] ; // [0]['numberitems'] ;//['#']['number'][0]['#'];// $datasetitems['#']['number'][0]['#']; |
546 | $qo->dataset[$qo->datasetindex]->datasetitem[$qo->dataset[$qo->datasetindex]->itemindex]->value = $datasetitem['#']['value'][0]['#'] ;//$datasetitem['#']['value'][0]['#']; |
547 | } |
548 | } |
549 | |
5fd8f999 |
550 | // echo "<pre>loaded qo";print_r($qo);echo "</pre>"; |
725ba2a0 |
551 | |
552 | return $qo; |
553 | } |
554 | |
ee259d0c |
555 | /** |
556 | * this is not a real question type. It's a dummy type used |
557 | * to specify the import category |
558 | * format is: |
559 | * <question type="category"> |
560 | * <category>tom/dick/harry</category> |
561 | * </question> |
562 | */ |
563 | function import_category( $question ) { |
0c6b4d2e |
564 | $qo = new stdClass; |
ee259d0c |
565 | $qo->qtype = 'category'; |
566 | $qo->category = $question['#']['category'][0]['#']; |
567 | return $qo; |
568 | } |
569 | |
c81415c7 |
570 | /** |
571 | * parse the array of lines into an array of questions |
572 | * this *could* burn memory - but it won't happen that much |
573 | * so fingers crossed! |
6e557c08 |
574 | * @param array lines array of lines from the input file |
575 | * @return array (of objects) question objects |
c81415c7 |
576 | */ |
577 | function readquestions($lines) { |
84769fd8 |
578 | // we just need it as one big string |
579 | $text = implode($lines, " "); |
580 | unset( $lines ); |
581 | |
582 | // this converts xml to big nasty data structure |
583 | // the 0 means keep white space as it is (important for markdown format) |
584 | // print_r it if you want to see what it looks like! |
585 | $xml = xmlize( $text, 0 ); |
586 | |
587 | // set up array to hold all our questions |
588 | $questions = array(); |
589 | |
590 | // iterate through questions |
591 | foreach ($xml['quiz']['#']['question'] as $question) { |
592 | $question_type = $question['@']['type']; |
593 | $questiontype = get_string( 'questiontype','quiz',$question_type ); |
84769fd8 |
594 | |
595 | if ($question_type=='multichoice') { |
596 | $qo = $this->import_multichoice( $question ); |
597 | } |
598 | elseif ($question_type=='truefalse') { |
599 | $qo = $this->import_truefalse( $question ); |
600 | } |
601 | elseif ($question_type=='shortanswer') { |
602 | $qo = $this->import_shortanswer( $question ); |
603 | } |
7b8bc256 |
604 | //elseif ($question_type=='regexp') { |
605 | // $qo = $this->import_regexp( $question ); |
606 | //} |
84769fd8 |
607 | elseif ($question_type=='numerical') { |
608 | $qo = $this->import_numerical( $question ); |
609 | } |
7b8bc256 |
610 | elseif ($question_type=='description') { |
611 | $qo = $this->import_description( $question ); |
612 | } |
51bcdf28 |
613 | elseif ($question_type=='matching') { |
614 | $qo = $this->import_matching( $question ); |
615 | } |
7b8bc256 |
616 | elseif ($question_type=='cloze') { |
c81415c7 |
617 | $qo = $this->import_multianswer( $question ); |
618 | } |
619 | elseif ($question_type=='essay') { |
620 | $qo = $this->import_essay( $question ); |
7b8bc256 |
621 | } |
725ba2a0 |
622 | elseif ($question_type=='calculated') { |
623 | $qo = $this->import_calculated( $question ); |
624 | } |
ee259d0c |
625 | elseif ($question_type=='category') { |
626 | $qo = $this->import_category( $question ); |
627 | } |
84769fd8 |
628 | else { |
ee800653 |
629 | $notsupported = get_string( 'xmltypeunsupported','quiz',$question_type ); |
46013523 |
630 | $this->error( $notsupported ); |
84769fd8 |
631 | $qo = null; |
632 | } |
633 | |
634 | // stick the result in the $questions array |
635 | if ($qo) { |
636 | $questions[] = $qo; |
637 | } |
638 | } |
84769fd8 |
639 | return $questions; |
640 | } |
641 | |
642 | // EXPORT FUNCTIONS START HERE |
643 | |
84769fd8 |
644 | function export_file_extension() { |
645 | // override default type so extension is .xml |
646 | |
647 | return ".xml"; |
648 | } |
649 | |
c81415c7 |
650 | |
651 | /** |
652 | * Turn the internal question code into a human readable form |
653 | * (The code used to be numeric, but this remains as some of |
654 | * the names don't match the new internal format) |
6e557c08 |
655 | * @param mixed type_id Internal code |
656 | * @return string question type string |
c81415c7 |
657 | */ |
84769fd8 |
658 | function get_qtype( $type_id ) { |
84769fd8 |
659 | switch( $type_id ) { |
660 | case TRUEFALSE: |
661 | $name = 'truefalse'; |
662 | break; |
663 | case MULTICHOICE: |
664 | $name = 'multichoice'; |
665 | break; |
666 | case SHORTANSWER: |
667 | $name = 'shortanswer'; |
668 | break; |
7b8bc256 |
669 | //case regexp: |
670 | // $name = 'regexp'; |
671 | // break; |
84769fd8 |
672 | case NUMERICAL: |
673 | $name = 'numerical'; |
674 | break; |
675 | case MATCH: |
676 | $name = 'matching'; |
677 | break; |
678 | case DESCRIPTION: |
679 | $name = 'description'; |
680 | break; |
681 | case MULTIANSWER: |
682 | $name = 'cloze'; |
683 | break; |
c81415c7 |
684 | case ESSAY: |
685 | $name = 'essay'; |
686 | break; |
725ba2a0 |
687 | case CALCULATED: |
688 | $name = 'calculated'; |
689 | break; |
84769fd8 |
690 | default: |
691 | $name = 'unknown'; |
692 | } |
693 | return $name; |
694 | } |
695 | |
6e557c08 |
696 | /** |
c81415c7 |
697 | * Convert internal Moodle text format code into |
698 | * human readable form |
6e557c08 |
699 | * @param int id internal code |
700 | * @return string format text |
c81415c7 |
701 | */ |
84769fd8 |
702 | function get_format( $id ) { |
84769fd8 |
703 | switch( $id ) { |
704 | case 0: |
705 | $name = "moodle_auto_format"; |
706 | break; |
707 | case 1: |
708 | $name = "html"; |
709 | break; |
710 | case 2: |
711 | $name = "plain_text"; |
712 | break; |
713 | case 3: |
714 | $name = "wiki_like"; |
715 | break; |
716 | case 4: |
717 | $name = "markdown"; |
718 | break; |
719 | default: |
720 | $name = "unknown"; |
721 | } |
722 | return $name; |
723 | } |
724 | |
6e557c08 |
725 | /** |
c81415c7 |
726 | * Convert internal single question code into |
727 | * human readable form |
6e557c08 |
728 | * @param int id single question code |
729 | * @return string single question string |
c81415c7 |
730 | */ |
84769fd8 |
731 | function get_single( $id ) { |
84769fd8 |
732 | switch( $id ) { |
733 | case 0: |
734 | $name = "false"; |
735 | break; |
736 | case 1: |
737 | $name = "true"; |
738 | break; |
739 | default: |
740 | $name = "unknown"; |
741 | } |
742 | return $name; |
743 | } |
744 | |
6e557c08 |
745 | /** |
c81415c7 |
746 | * generates <text></text> tags, processing raw text therein |
6e557c08 |
747 | * @param int ilev the current indent level |
748 | * @param boolean short stick it on one line |
749 | * @return string formatted text |
c81415c7 |
750 | */ |
84769fd8 |
751 | function writetext( $raw, $ilev=0, $short=true) { |
84769fd8 |
752 | $indent = str_repeat( " ",$ilev ); |
753 | |
754 | // encode the text to 'disguise' HTML content |
755 | $raw = htmlspecialchars( $raw ); |
756 | |
757 | if ($short) { |
758 | $xml = "$indent<text>$raw</text>\n"; |
759 | } |
760 | else { |
761 | $xml = "$indent<text>\n$raw\n$indent</text>\n"; |
762 | } |
763 | |
764 | return $xml; |
765 | } |
766 | |
767 | function presave_process( $content ) { |
768 | // override method to allow us to add xml headers and footers |
769 | |
46013523 |
770 | $content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" . |
84769fd8 |
771 | "<quiz>\n" . |
772 | $content . "\n" . |
773 | "</quiz>"; |
774 | |
775 | return $content; |
776 | } |
777 | |
6e557c08 |
778 | /** |
c81415c7 |
779 | * Include an image encoded in base 64 |
6e557c08 |
780 | * @param string imagepath The location of the image file |
781 | * @return string xml code segment |
c81415c7 |
782 | */ |
d08e16b2 |
783 | function writeimage( $imagepath ) { |
d08e16b2 |
784 | global $CFG; |
785 | |
786 | if (empty($imagepath)) { |
787 | return ''; |
788 | } |
789 | |
790 | $courseid = $this->course->id; |
791 | if (!$binary = file_get_contents( "{$CFG->dataroot}/$courseid/$imagepath" )) { |
792 | return ''; |
793 | } |
794 | |
795 | $content = " <image_base64>\n".addslashes(base64_encode( $binary ))."\n". |
796 | "\n </image_base64>\n"; |
797 | return $content; |
798 | } |
799 | |
6e557c08 |
800 | /** |
c81415c7 |
801 | * Turns question into an xml segment |
6e557c08 |
802 | * @param array question question array |
803 | * @return string xml segment |
c81415c7 |
804 | */ |
84769fd8 |
805 | function writequestion( $question ) { |
725ba2a0 |
806 | global $CFG,$QTYPES; |
84769fd8 |
807 | // initial string; |
808 | $expout = ""; |
809 | |
810 | // add comment |
811 | $expout .= "\n\n<!-- question: $question->id -->\n"; |
812 | |
46013523 |
813 | // check question type - make sure valid |
814 | $question_type = $this->get_qtype( $question->qtype ); |
815 | if ($question_type=='unknown') { |
816 | $expout .= "<!-- question: $question->name is not a supported type -->\n\n"; |
817 | } |
818 | |
84769fd8 |
819 | // add opening tag |
f1abd39f |
820 | // generates specific header for Cloze and category type question |
821 | if ($question->qtype == 'category') { |
822 | $expout .= " <question type=\"category\">\n"; |
823 | $expout .= " <category>\n"; |
824 | $expout .= " $question->category\n"; |
825 | $expout .= " </category>\n"; |
826 | $expout .= " </question>\n"; |
827 | return $expout; |
828 | } |
829 | elseif ($question->qtype != MULTIANSWER) { |
7b8bc256 |
830 | // for all question types except Close |
7b8bc256 |
831 | $name_text = $this->writetext( $question->name ); |
832 | $qtformat = $this->get_format($question->questiontextformat); |
833 | $question_text = $this->writetext( $question->questiontext ); |
a4514d91 |
834 | $generalfeedback = $this->writetext( $question->generalfeedback ); |
7b8bc256 |
835 | $expout .= " <question type=\"$question_type\">\n"; |
836 | $expout .= " <name>$name_text</name>\n"; |
837 | $expout .= " <questiontext format=\"$qtformat\">\n"; |
838 | $expout .= $question_text; |
839 | $expout .= " </questiontext>\n"; |
840 | $expout .= " <image>{$question->image}</image>\n"; |
841 | $expout .= $this->writeimage($question->image); |
a4514d91 |
842 | $expout .= " <generalfeedback>\n"; |
843 | $expout .= $generalfeedback; |
844 | $expout .= " </generalfeedback>\n"; |
c81415c7 |
845 | $expout .= " <defaultgrade>{$question->defaultgrade}</defaultgrade>\n"; |
7b8bc256 |
846 | $expout .= " <penalty>{$question->penalty}</penalty>\n"; |
847 | $expout .= " <hidden>{$question->hidden}</hidden>\n"; |
848 | } |
849 | else { |
850 | // for Cloze type only |
7b8bc256 |
851 | $name_text = $this->writetext( $question->name ); |
852 | $question_text = $this->writetext( $question->questiontext ); |
853 | $expout .= " <question type=\"$question_type\">\n"; |
854 | $expout .= " <name>$name_text</name>\n"; |
855 | $expout .= " <questiontext>\n"; |
856 | $expout .= $question_text; |
857 | $expout .= " </questiontext>\n"; |
858 | } |
859 | |
d08e16b2 |
860 | if (!empty($question->options->shuffleanswers)) { |
861 | $expout .= " <shuffleanswers>{$question->options->shuffleanswers}</shuffleanswers>\n"; |
862 | } |
863 | else { |
864 | $expout .= " <shuffleanswers>0</shuffleanswers>\n"; |
865 | } |
84769fd8 |
866 | |
867 | // output depends on question type |
868 | switch($question->qtype) { |
f1abd39f |
869 | case 'category': |
870 | // not a qtype really - dummy used for category switching |
871 | break; |
84769fd8 |
872 | case TRUEFALSE: |
36e2232e |
873 | foreach ($question->options->answers as $answer) { |
874 | $fraction_pc = round( $answer->fraction * 100 ); |
3246ed33 |
875 | if ($answer->id == $question->options->trueanswer) { |
876 | $answertext = 'true'; |
877 | } else { |
878 | $answertext = 'false'; |
879 | } |
36e2232e |
880 | $expout .= " <answer fraction=\"$fraction_pc\">\n"; |
3246ed33 |
881 | $expout .= $this->writetext($answertext, 3) . "\n"; |
36e2232e |
882 | $expout .= " <feedback>\n"; |
883 | $expout .= $this->writetext( $answer->feedback,4,false ); |
884 | $expout .= " </feedback>\n"; |
885 | $expout .= " </answer>\n"; |
886 | } |
84769fd8 |
887 | break; |
888 | case MULTICHOICE: |
889 | $expout .= " <single>".$this->get_single($question->options->single)."</single>\n"; |
307f045f |
890 | $expout .= " <shuffleanswers>".$this->get_single($question->options->shuffleanswers)."</shuffleanswers>\n"; |
891 | $expout .= " <correctfeedback>".$this->writetext($question->options->correctfeedback, 3)."</correctfeedback>\n"; |
892 | $expout .= " <partiallycorrectfeedback>".$this->writetext($question->options->partiallycorrectfeedback, 3)."</partiallycorrectfeedback>\n"; |
893 | $expout .= " <incorrectfeedback>".$this->writetext($question->options->incorrectfeedback, 3)."</incorrectfeedback>\n"; |
84769fd8 |
894 | foreach($question->options->answers as $answer) { |
895 | $percent = $answer->fraction * 100; |
896 | $expout .= " <answer fraction=\"$percent\">\n"; |
897 | $expout .= $this->writetext( $answer->answer,4,false ); |
898 | $expout .= " <feedback>\n"; |
6e557c08 |
899 | $expout .= $this->writetext( $answer->feedback,5,false ); |
84769fd8 |
900 | $expout .= " </feedback>\n"; |
901 | $expout .= " </answer>\n"; |
902 | } |
903 | break; |
904 | case SHORTANSWER: |
905 | $expout .= " <usecase>{$question->options->usecase}</usecase>\n "; |
906 | foreach($question->options->answers as $answer) { |
907 | $percent = 100 * $answer->fraction; |
908 | $expout .= " <answer fraction=\"$percent\">\n"; |
909 | $expout .= $this->writetext( $answer->answer,3,false ); |
910 | $expout .= " <feedback>\n"; |
911 | $expout .= $this->writetext( $answer->feedback,4,false ); |
912 | $expout .= " </feedback>\n"; |
913 | $expout .= " </answer>\n"; |
914 | } |
915 | break; |
7b8bc256 |
916 | //case regexp: |
917 | //$expout .= " <usecase>{$question->options->usecase}</usecase>\n "; |
918 | // foreach($question->options->answers as $answer) { |
919 | // $percent = 100 * $answer->fraction; |
920 | // $expout .= " <answer fraction=\"$percent\">\n"; |
921 | // $expout .= $this->writetext( $answer->answer,3,false ); |
922 | // $expout .= " <feedback>\n"; |
923 | // $expout .= $this->writetext( $answer->feedback,4,false ); |
924 | // $expout .= " </feedback>\n"; |
925 | // $expout .= " </answer>\n"; |
926 | // } |
927 | // break; |
84769fd8 |
928 | case NUMERICAL: |
929 | foreach ($question->options->answers as $answer) { |
930 | $tolerance = $answer->tolerance; |
55c54868 |
931 | $percent = 100 * $answer->fraction; |
932 | $expout .= "<answer fraction=\"$percent\">\n"; |
933 | // <text> tags are an added feature, old filed won't have them |
934 | $expout .= " <text>{$answer->answer}</text>\n"; |
84769fd8 |
935 | $expout .= " <tolerance>$tolerance</tolerance>\n"; |
936 | $expout .= " <feedback>".$this->writetext( $answer->feedback )."</feedback>\n"; |
55c54868 |
937 | // fraction tag is deprecated |
938 | // $expout .= " <fraction>{$answer->fraction}</fraction>\n"; |
84769fd8 |
939 | $expout .= "</answer>\n"; |
940 | } |
941 | |
942 | $units = $question->options->units; |
943 | if (count($units)) { |
944 | $expout .= "<units>\n"; |
945 | foreach ($units as $unit) { |
946 | $expout .= " <unit>\n"; |
947 | $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n"; |
948 | $expout .= " <unit_name>{$unit->unit}</unit_name>\n"; |
949 | $expout .= " </unit>\n"; |
950 | } |
951 | $expout .= "</units>\n"; |
952 | } |
953 | break; |
954 | case MATCH: |
955 | foreach($question->options->subquestions as $subquestion) { |
956 | $expout .= "<subquestion>\n"; |
957 | $expout .= $this->writetext( $subquestion->questiontext ); |
958 | $expout .= "<answer>".$this->writetext( $subquestion->answertext )."</answer>\n"; |
959 | $expout .= "</subquestion>\n"; |
960 | } |
961 | break; |
962 | case DESCRIPTION: |
c81415c7 |
963 | // nothing more to do for this type |
84769fd8 |
964 | break; |
965 | case MULTIANSWER: |
7b8bc256 |
966 | $a_count=1; |
967 | foreach($question->options->questions as $question) { |
968 | $thispattern = addslashes("{#".$a_count."}"); |
969 | $thisreplace = $question->questiontext; |
970 | $expout=ereg_replace($thispattern, $thisreplace, $expout ); |
971 | $a_count++; |
972 | } |
973 | break; |
c81415c7 |
974 | case ESSAY: |
975 | foreach ($question->options->answers as $answer) { |
55c54868 |
976 | $percent = 100 * $answer->fraction; |
977 | $expout .= "<answer fraction=\"$percent\">\n"; |
c81415c7 |
978 | $expout .= " <feedback>".$this->writetext( $answer->feedback )."</feedback>\n"; |
55c54868 |
979 | // fraction tag is deprecated |
980 | // $expout .= " <fraction>{$answer->fraction}</fraction>\n"; |
c81415c7 |
981 | $expout .= "</answer>\n"; |
982 | } |
983 | |
725ba2a0 |
984 | break; |
985 | case CALCULATED: |
986 | foreach ($question->options->answers as $answer) { |
987 | $tolerance = $answer->tolerance; |
988 | $tolerancetype = $answer->tolerancetype; |
989 | $correctanswerlength= $answer->correctanswerlength ; |
990 | $correctanswerformat= $answer->correctanswerformat; |
991 | $percent = 100 * $answer->fraction; |
992 | $expout .= "<answer fraction=\"$percent\">\n"; |
993 | // "<text/>" tags are an added feature, old files won't have them |
994 | $expout .= " <text>{$answer->answer}</text>\n"; |
995 | $expout .= " <tolerance>$tolerance</tolerance>\n"; |
996 | $expout .= " <tolerancetype>$tolerancetype</tolerancetype>\n"; |
997 | $expout .= " <correctanswerformat>$correctanswerformat</correctanswerformat>\n"; |
998 | $expout .= " <correctanswerlength>$correctanswerformat</correctanswerlength>\n"; |
999 | $expout .= " <feedback>".$this->writetext( $answer->feedback )."</feedback>\n"; |
1000 | $expout .= "</answer>\n"; |
1001 | } |
1002 | $units = $question->options->units; |
1003 | if (count($units)) { |
1004 | $expout .= "<units>\n"; |
1005 | foreach ($units as $unit) { |
1006 | $expout .= " <unit>\n"; |
1007 | $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n"; |
1008 | $expout .= " <unit_name>{$unit->unit}</unit_name>\n"; |
1009 | $expout .= " </unit>\n"; |
1010 | } |
1011 | $expout .= "</units>\n"; |
1012 | } |
1013 | //echo "<pre> question calc";print_r($question);echo "</pre>"; |
1014 | //First, we a new function to get all the data itmes in the database |
c9e4ba36 |
1015 | // $question_datasetdefs =$QTYPES['calculated']->get_datasets_for_export ($question); |
725ba2a0 |
1016 | // echo "<pre> question defs";print_r($question_datasetdefs);echo "</pre>"; |
1017 | //If there are question_datasets |
c9e4ba36 |
1018 | if( isset($question->options->datasets)&&count($question->options->datasets)){// there should be |
725ba2a0 |
1019 | $expout .= "<dataset_definitions>\n"; |
c9e4ba36 |
1020 | foreach ($question->options->datasets as $def) { |
725ba2a0 |
1021 | $expout .= "<dataset_definition>\n"; |
1022 | $expout .= " <status>".$this->writetext($def->status)."</status>\n"; |
1023 | $expout .= " <name>".$this->writetext($def->name)."</name>\n"; |
1024 | $expout .= " <type>calculated</type>\n"; |
1025 | $expout .= " <distribution>".$this->writetext($def->distribution)."</distribution>\n"; |
1026 | $expout .= " <minimum>".$this->writetext($def->minimum)."</minimum>\n"; |
1027 | $expout .= " <maximum>".$this->writetext($def->maximum)."</maximum>\n"; |
1028 | $expout .= " <decimals>".$this->writetext($def->decimals)."</decimals>\n"; |
1029 | $expout .= " <itemcount>$def->itemcount</itemcount>\n"; |
1030 | if ($def->itemcount > 0 ) { |
1031 | $expout .= " <dataset_items>\n"; |
1032 | foreach ($def->items as $item ){ |
1033 | $expout .= " <dataset_item>\n"; |
1034 | $expout .= " <number>".$item->itemnumber."</number>\n"; |
1035 | $expout .= " <value>".$item->value."</value>\n"; |
1036 | $expout .= " </dataset_item>\n"; |
1037 | } |
1038 | $expout .= " </dataset_items>\n"; |
1039 | $expout .= " <number_of_items>".$def-> number_of_items."</number_of_items>\n"; |
1040 | } |
1041 | $expout .= "</dataset_definition>\n"; |
1042 | } |
1043 | $expout .= "</dataset_definitions>\n"; |
1044 | } |
c81415c7 |
1045 | break; |
84769fd8 |
1046 | default: |
46013523 |
1047 | // should not get here |
1048 | error( 'Unsupported question type detected in strange circumstances!' ); |
84769fd8 |
1049 | } |
1050 | |
1051 | // close the question tag |
1052 | $expout .= "</question>\n"; |
1053 | |
84769fd8 |
1054 | return $expout; |
1055 | } |
1056 | } |
1057 | |
1058 | ?> |