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 | |
6e557c08 |
100 | /** |
c81415c7 |
101 | * import parts of question common to all types |
6e557c08 |
102 | * @param array question question array from xml tree |
103 | * @return object question object |
c81415c7 |
104 | */ |
84769fd8 |
105 | function import_headers( $question ) { |
84769fd8 |
106 | // this routine initialises the question object |
5bed54e1 |
107 | $qo = $this->defaultquestion(); |
84769fd8 |
108 | $name = $this->import_text( $question['#']['name'][0]['#']['text'] ); |
109 | $qtext = $this->import_text( $question['#']['questiontext'][0]['#']['text'] ); |
110 | $qformat = $question['#']['questiontext'][0]['@']['format']; |
111 | $image = $question['#']['image'][0]['#']; |
d08e16b2 |
112 | if (!empty($question['#']['image_base64'][0]['#'])) { |
113 | $image_base64 = stripslashes( trim( $question['#']['image_base64'][0]['#'] ) ); |
114 | $image = $this->importimagefile( $image, $image_base64 ); |
115 | } |
a4514d91 |
116 | if (array_key_exists('generalfeedback', $question['#'])) { |
117 | $generalfeedback = $this->import_text( $question['#']['generalfeedback'][0]['#']['text'] ); |
1b8a7434 |
118 | } else { |
a4514d91 |
119 | $generalfeedback = ''; |
1b8a7434 |
120 | } |
c81415c7 |
121 | if (!empty($question['#']['defaultgrade'][0]['#'])) { |
122 | $qo->defaultgrade = $question['#']['defaultgrade'][0]['#']; |
123 | } |
5bed54e1 |
124 | |
84769fd8 |
125 | $penalty = $question['#']['penalty'][0]['#']; |
126 | |
84769fd8 |
127 | $qo->name = $name; |
128 | $qo->questiontext = $qtext; |
129 | $qo->questiontextformat = $this->trans_format( $qformat ); |
130 | $qo->image = ((!empty($image)) ? $image : ''); |
a4514d91 |
131 | $qo->generalfeedback = $generalfeedback; |
84769fd8 |
132 | $qo->penalty = $penalty; |
133 | |
134 | return $qo; |
135 | } |
136 | |
6e557c08 |
137 | /** |
c81415c7 |
138 | * import the common parts of a single answer |
6e557c08 |
139 | * @param array answer xml tree for single answer |
140 | * @return object answer object |
c81415c7 |
141 | */ |
84769fd8 |
142 | function import_answer( $answer ) { |
84769fd8 |
143 | $fraction = $answer['@']['fraction']; |
144 | $text = $this->import_text( $answer['#']['text']); |
145 | $feedback = $this->import_text( $answer['#']['feedback'][0]['#']['text'] ); |
146 | |
147 | $ans = null; |
148 | $ans->answer = $text; |
149 | $ans->fraction = $fraction / 100; |
150 | $ans->feedback = $feedback; |
151 | |
152 | return $ans; |
153 | } |
154 | |
6e557c08 |
155 | /** |
c81415c7 |
156 | * import multiple choice question |
6e557c08 |
157 | * @param array question question array from xml tree |
158 | * @return object question object |
c81415c7 |
159 | */ |
84769fd8 |
160 | function import_multichoice( $question ) { |
84769fd8 |
161 | // get common parts |
162 | $qo = $this->import_headers( $question ); |
163 | |
164 | // 'header' parts particular to multichoice |
165 | $qo->qtype = MULTICHOICE; |
166 | $single = $question['#']['single'][0]['#']; |
167 | $qo->single = $this->trans_single( $single ); |
307f045f |
168 | if (array_key_exists('shuffleanswers', $question['#'])) { |
169 | $shuffleanswers = $question['#']['shuffleanswers'][0]['#']; |
170 | } else { |
171 | $shuffleanswers = 'false'; |
172 | } |
2da44816 |
173 | $qo->shuffleanswers = $this->trans_single($shuffleanswers); |
307f045f |
174 | $qo->correctfeedback = $this->import_optional_text('correctfeedback', $question); |
175 | $qo->partiallycorrectfeedback = $this->import_optional_text('partiallycorrectfeedback', $question); |
176 | $qo->incorrectfeedback = $this->import_optional_text('incorrectfeedback', $question); |
177 | |
84769fd8 |
178 | // run through the answers |
179 | $answers = $question['#']['answer']; |
180 | $a_count = 0; |
181 | foreach ($answers as $answer) { |
182 | $ans = $this->import_answer( $answer ); |
183 | $qo->answer[$a_count] = $ans->answer; |
184 | $qo->fraction[$a_count] = $ans->fraction; |
185 | $qo->feedback[$a_count] = $ans->feedback; |
186 | ++$a_count; |
187 | } |
188 | |
189 | return $qo; |
190 | } |
191 | |
6e557c08 |
192 | /** |
c81415c7 |
193 | * import cloze type question |
6e557c08 |
194 | * @param array question question array from xml tree |
195 | * @return object question object |
c81415c7 |
196 | */ |
7b8bc256 |
197 | function import_multianswer( $questions ) { |
0c6b4d2e |
198 | $questiontext = $questions['#']['questiontext'][0]['#']['text']; |
199 | $qo = qtype_multianswer_extract_question($this->import_text($questiontext)); |
7b8bc256 |
200 | |
201 | // 'header' parts particular to multianswer |
202 | $qo->qtype = MULTIANSWER; |
203 | $qo->course = $this->course; |
204 | |
71ffbac2 |
205 | if (!empty($questions)) { |
7b8bc256 |
206 | $qo->name = $this->import_text( $questions['#']['name'][0]['#']['text'] ); |
207 | } |
208 | |
209 | return $qo; |
210 | } |
211 | |
6e557c08 |
212 | /** |
c81415c7 |
213 | * import true/false type question |
6e557c08 |
214 | * @param array question question array from xml tree |
215 | * @return object question object |
c81415c7 |
216 | */ |
84769fd8 |
217 | function import_truefalse( $question ) { |
84769fd8 |
218 | // get common parts |
219 | $qo = $this->import_headers( $question ); |
220 | |
221 | // 'header' parts particular to true/false |
222 | $qo->qtype = TRUEFALSE; |
223 | |
224 | // get answer info |
3246ed33 |
225 | // |
226 | // In the past, it used to be assumed that the two answers were in the file |
227 | // true first, then false. Howevever that was not always true. Now, we |
228 | // try to match on the answer text, but in old exports, this will be a localised |
229 | // string, so if we don't find true or false, we fall back to the old system. |
230 | $first = true; |
231 | $warning = false; |
232 | foreach ($question['#']['answer'] as $answer) { |
233 | $answertext = $this->import_text($answer['#']['text']); |
234 | $feedback = $this->import_text($answer['#']['feedback'][0]['#']['text']); |
235 | if ($answertext != 'true' && $answertext != 'false') { |
236 | $warning = true; |
237 | $answertext = $first ? 'true' : 'false'; // Old style file, assume order is true/false. |
238 | } |
239 | if ($answertext == 'true') { |
240 | $qo->answer = ($answer['@']['fraction'] == 100); |
7939a4a0 |
241 | $qo->correctanswer = $qo->answer; |
3246ed33 |
242 | $qo->feedbacktrue = $feedback; |
243 | } else { |
244 | $qo->answer = ($answer['@']['fraction'] != 100); |
7939a4a0 |
245 | $qo->correctanswer = $qo->answer; |
3246ed33 |
246 | $qo->feedbackfalse = $feedback; |
247 | } |
248 | $first = false; |
84769fd8 |
249 | } |
3246ed33 |
250 | |
251 | if ($warning) { |
252 | $a = new stdClass; |
55c54868 |
253 | $a->questiontext = $qo->questiontext; |
3246ed33 |
254 | $a->answer = get_string($qo->answer ? 'true' : 'false', 'quiz'); |
255 | notify(get_string('truefalseimporterror', 'quiz', $a)); |
84769fd8 |
256 | } |
257 | |
258 | return $qo; |
259 | } |
260 | |
6e557c08 |
261 | /** |
c81415c7 |
262 | * import short answer type question |
6e557c08 |
263 | * @param array question question array from xml tree |
264 | * @return object question object |
c81415c7 |
265 | */ |
84769fd8 |
266 | function import_shortanswer( $question ) { |
84769fd8 |
267 | // get common parts |
268 | $qo = $this->import_headers( $question ); |
269 | |
270 | // header parts particular to shortanswer |
271 | $qo->qtype = SHORTANSWER; |
272 | |
273 | // get usecase |
274 | $qo->usecase = $question['#']['usecase'][0]['#']; |
275 | |
276 | // run through the answers |
277 | $answers = $question['#']['answer']; |
278 | $a_count = 0; |
279 | foreach ($answers as $answer) { |
280 | $ans = $this->import_answer( $answer ); |
281 | $qo->answer[$a_count] = $ans->answer; |
282 | $qo->fraction[$a_count] = $ans->fraction; |
283 | $qo->feedback[$a_count] = $ans->feedback; |
284 | ++$a_count; |
285 | } |
286 | |
287 | return $qo; |
288 | } |
7b8bc256 |
289 | |
6e557c08 |
290 | /** |
c81415c7 |
291 | * import regexp type question |
6e557c08 |
292 | * @param array question question array from xml tree |
293 | * @return object question object |
c81415c7 |
294 | */ |
7b8bc256 |
295 | function import_regexp( $question ) { |
7b8bc256 |
296 | // get common parts |
297 | $qo = $this->import_headers( $question ); |
298 | |
299 | // header parts particular to shortanswer |
300 | $qo->qtype = regexp; |
301 | |
302 | // get usecase |
303 | $qo->usecase = $question['#']['usecase'][0]['#']; |
304 | |
305 | // run through the answers |
306 | $answers = $question['#']['answer']; |
307 | $a_count = 0; |
308 | foreach ($answers as $answer) { |
309 | $ans = $this->import_answer( $answer ); |
310 | $qo->answer[$a_count] = $ans->answer; |
311 | $qo->fraction[$a_count] = $ans->fraction; |
312 | $qo->feedback[$a_count] = $ans->feedback; |
313 | ++$a_count; |
314 | } |
84769fd8 |
315 | |
7b8bc256 |
316 | return $qo; |
317 | } |
318 | |
6e557c08 |
319 | /** |
c81415c7 |
320 | * import description type question |
6e557c08 |
321 | * @param array question question array from xml tree |
322 | * @return object question object |
c81415c7 |
323 | */ |
7b8bc256 |
324 | function import_description( $question ) { |
325 | // get common parts |
326 | $qo = $this->import_headers( $question ); |
327 | // header parts particular to shortanswer |
328 | $qo->qtype = DESCRIPTION; |
329 | return $qo; |
330 | } |
84769fd8 |
331 | |
6e557c08 |
332 | /** |
c81415c7 |
333 | * import numerical type question |
6e557c08 |
334 | * @param array question question array from xml tree |
335 | * @return object question object |
c81415c7 |
336 | */ |
337 | function import_numerical( $question ) { |
84769fd8 |
338 | // get common parts |
339 | $qo = $this->import_headers( $question ); |
340 | |
341 | // header parts particular to numerical |
342 | $qo->qtype = NUMERICAL; |
343 | |
344 | // get answers array |
345 | $answers = $question['#']['answer']; |
346 | $qo->answer = array(); |
347 | $qo->feedback = array(); |
348 | $qo->fraction = array(); |
349 | $qo->tolerance = array(); |
350 | foreach ($answers as $answer) { |
55c54868 |
351 | // answer outside of <text> is deprecated |
352 | if (!empty( $answer['#']['text'] )) { |
353 | $answertext = $this->import_text( $answer['#']['text'] ); |
354 | } |
355 | else { |
356 | $answertext = trim($answer['#'][0]); |
357 | } |
55894a42 |
358 | if ($answertext == '') { |
359 | $qo->answer[] = '*'; |
360 | } else { |
361 | $qo->answer[] = $answertext; |
362 | } |
a0d187bf |
363 | $qo->feedback[] = $this->import_text( $answer['#']['feedback'][0]['#']['text'] ); |
84769fd8 |
364 | $qo->tolerance[] = $answer['#']['tolerance'][0]['#']; |
55c54868 |
365 | |
366 | // fraction as a tag is deprecated |
367 | if (!empty($answer['#']['fraction'][0]['#'])) { |
368 | $qo->fraction[] = $answer['#']['fraction'][0]['#']; |
369 | } |
370 | else { |
371 | $qo->fraction[] = $answer['@']['fraction'] / 100; |
372 | } |
84769fd8 |
373 | } |
374 | |
375 | // get units array |
84769fd8 |
376 | $qo->unit = array(); |
a0d187bf |
377 | if (isset($question['#']['units'][0]['#']['unit'])) { |
378 | $units = $question['#']['units'][0]['#']['unit']; |
379 | $qo->multiplier = array(); |
380 | foreach ($units as $unit) { |
381 | $qo->multiplier[] = $unit['#']['multiplier'][0]['#']; |
382 | $qo->unit[] = $unit['#']['unit_name'][0]['#']; |
383 | } |
84769fd8 |
384 | } |
84769fd8 |
385 | return $qo; |
386 | } |
387 | |
6e557c08 |
388 | /** |
c81415c7 |
389 | * import matching type question |
6e557c08 |
390 | * @param array question question array from xml tree |
391 | * @return object question object |
c81415c7 |
392 | */ |
51bcdf28 |
393 | function import_matching( $question ) { |
51bcdf28 |
394 | // get common parts |
395 | $qo = $this->import_headers( $question ); |
396 | |
397 | // header parts particular to matching |
398 | $qo->qtype = MATCH; |
ee800653 |
399 | if (!empty($question['#']['shuffleanswers'])) { |
400 | $qo->shuffleanswers = $question['#']['shuffleanswers'][0]['#']; |
401 | } else { |
402 | $qo->shuffleanswers = false; |
403 | } |
51bcdf28 |
404 | |
405 | // get subquestions |
406 | $subquestions = $question['#']['subquestion']; |
407 | $qo->subquestions = array(); |
408 | $qo->subanswers = array(); |
409 | |
410 | // run through subquestions |
411 | foreach ($subquestions as $subquestion) { |
a0d187bf |
412 | $qtext = $this->import_text( $subquestion['#']['text'] ); |
413 | $atext = $this->import_text( $subquestion['#']['answer'][0]['#']['text'] ); |
51bcdf28 |
414 | $qo->subquestions[] = $qtext; |
415 | $qo->subanswers[] = $atext; |
416 | } |
51bcdf28 |
417 | return $qo; |
418 | } |
419 | |
6e557c08 |
420 | /** |
c81415c7 |
421 | * import essay type question |
6e557c08 |
422 | * @param array question question array from xml tree |
423 | * @return object question object |
c81415c7 |
424 | */ |
425 | function import_essay( $question ) { |
426 | // get common parts |
427 | $qo = $this->import_headers( $question ); |
428 | |
429 | // header parts particular to essay |
430 | $qo->qtype = ESSAY; |
431 | |
432 | // get feedback |
433 | $qo->feedback = $this->import_text( $question['#']['answer'][0]['#']['feedback'][0]['#']['text'] ); |
434 | |
55c54868 |
435 | // handle answer |
436 | $answer = $question['#']['answer'][0]; |
437 | |
438 | // get fraction - <fraction> tag is deprecated |
439 | if (!empty($answer['#']['fraction'][0]['#'])) { |
440 | $qo->fraction = $answer['#']['fraction'][0]['#']; |
441 | } |
442 | else { |
443 | $qo->fraction = $answer['@']['fraction'] / 100; |
444 | } |
c81415c7 |
445 | |
446 | return $qo; |
447 | } |
84769fd8 |
448 | |
ee259d0c |
449 | /** |
450 | * this is not a real question type. It's a dummy type used |
451 | * to specify the import category |
452 | * format is: |
453 | * <question type="category"> |
454 | * <category>tom/dick/harry</category> |
455 | * </question> |
456 | */ |
457 | function import_category( $question ) { |
0c6b4d2e |
458 | $qo = new stdClass; |
ee259d0c |
459 | $qo->qtype = 'category'; |
460 | $qo->category = $question['#']['category'][0]['#']; |
461 | return $qo; |
462 | } |
463 | |
c81415c7 |
464 | /** |
465 | * parse the array of lines into an array of questions |
466 | * this *could* burn memory - but it won't happen that much |
467 | * so fingers crossed! |
6e557c08 |
468 | * @param array lines array of lines from the input file |
469 | * @return array (of objects) question objects |
c81415c7 |
470 | */ |
471 | function readquestions($lines) { |
84769fd8 |
472 | // we just need it as one big string |
473 | $text = implode($lines, " "); |
474 | unset( $lines ); |
475 | |
476 | // this converts xml to big nasty data structure |
477 | // the 0 means keep white space as it is (important for markdown format) |
478 | // print_r it if you want to see what it looks like! |
479 | $xml = xmlize( $text, 0 ); |
480 | |
481 | // set up array to hold all our questions |
482 | $questions = array(); |
483 | |
484 | // iterate through questions |
485 | foreach ($xml['quiz']['#']['question'] as $question) { |
486 | $question_type = $question['@']['type']; |
487 | $questiontype = get_string( 'questiontype','quiz',$question_type ); |
84769fd8 |
488 | |
489 | if ($question_type=='multichoice') { |
490 | $qo = $this->import_multichoice( $question ); |
491 | } |
492 | elseif ($question_type=='truefalse') { |
493 | $qo = $this->import_truefalse( $question ); |
494 | } |
495 | elseif ($question_type=='shortanswer') { |
496 | $qo = $this->import_shortanswer( $question ); |
497 | } |
7b8bc256 |
498 | //elseif ($question_type=='regexp') { |
499 | // $qo = $this->import_regexp( $question ); |
500 | //} |
84769fd8 |
501 | elseif ($question_type=='numerical') { |
502 | $qo = $this->import_numerical( $question ); |
503 | } |
7b8bc256 |
504 | elseif ($question_type=='description') { |
505 | $qo = $this->import_description( $question ); |
506 | } |
51bcdf28 |
507 | elseif ($question_type=='matching') { |
508 | $qo = $this->import_matching( $question ); |
509 | } |
7b8bc256 |
510 | elseif ($question_type=='cloze') { |
c81415c7 |
511 | $qo = $this->import_multianswer( $question ); |
512 | } |
513 | elseif ($question_type=='essay') { |
514 | $qo = $this->import_essay( $question ); |
7b8bc256 |
515 | } |
ee259d0c |
516 | elseif ($question_type=='category') { |
517 | $qo = $this->import_category( $question ); |
518 | } |
84769fd8 |
519 | else { |
ee800653 |
520 | $notsupported = get_string( 'xmltypeunsupported','quiz',$question_type ); |
4089729b |
521 | $this->error( $notsupportted ); |
84769fd8 |
522 | $qo = null; |
523 | } |
524 | |
525 | // stick the result in the $questions array |
526 | if ($qo) { |
0c6b4d2e |
527 | $qo->generalfeedback = ''; |
84769fd8 |
528 | $questions[] = $qo; |
529 | } |
530 | } |
84769fd8 |
531 | return $questions; |
532 | } |
533 | |
534 | // EXPORT FUNCTIONS START HERE |
535 | |
84769fd8 |
536 | function export_file_extension() { |
537 | // override default type so extension is .xml |
538 | |
539 | return ".xml"; |
540 | } |
541 | |
c81415c7 |
542 | |
543 | /** |
544 | * Turn the internal question code into a human readable form |
545 | * (The code used to be numeric, but this remains as some of |
546 | * the names don't match the new internal format) |
6e557c08 |
547 | * @param mixed type_id Internal code |
548 | * @return string question type string |
c81415c7 |
549 | */ |
84769fd8 |
550 | function get_qtype( $type_id ) { |
84769fd8 |
551 | switch( $type_id ) { |
552 | case TRUEFALSE: |
553 | $name = 'truefalse'; |
554 | break; |
555 | case MULTICHOICE: |
556 | $name = 'multichoice'; |
557 | break; |
558 | case SHORTANSWER: |
559 | $name = 'shortanswer'; |
560 | break; |
7b8bc256 |
561 | //case regexp: |
562 | // $name = 'regexp'; |
563 | // break; |
84769fd8 |
564 | case NUMERICAL: |
565 | $name = 'numerical'; |
566 | break; |
567 | case MATCH: |
568 | $name = 'matching'; |
569 | break; |
570 | case DESCRIPTION: |
571 | $name = 'description'; |
572 | break; |
573 | case MULTIANSWER: |
574 | $name = 'cloze'; |
575 | break; |
c81415c7 |
576 | case ESSAY: |
577 | $name = 'essay'; |
578 | break; |
84769fd8 |
579 | default: |
580 | $name = 'unknown'; |
581 | } |
582 | return $name; |
583 | } |
584 | |
6e557c08 |
585 | /** |
c81415c7 |
586 | * Convert internal Moodle text format code into |
587 | * human readable form |
6e557c08 |
588 | * @param int id internal code |
589 | * @return string format text |
c81415c7 |
590 | */ |
84769fd8 |
591 | function get_format( $id ) { |
84769fd8 |
592 | switch( $id ) { |
593 | case 0: |
594 | $name = "moodle_auto_format"; |
595 | break; |
596 | case 1: |
597 | $name = "html"; |
598 | break; |
599 | case 2: |
600 | $name = "plain_text"; |
601 | break; |
602 | case 3: |
603 | $name = "wiki_like"; |
604 | break; |
605 | case 4: |
606 | $name = "markdown"; |
607 | break; |
608 | default: |
609 | $name = "unknown"; |
610 | } |
611 | return $name; |
612 | } |
613 | |
6e557c08 |
614 | /** |
c81415c7 |
615 | * Convert internal single question code into |
616 | * human readable form |
6e557c08 |
617 | * @param int id single question code |
618 | * @return string single question string |
c81415c7 |
619 | */ |
84769fd8 |
620 | function get_single( $id ) { |
84769fd8 |
621 | switch( $id ) { |
622 | case 0: |
623 | $name = "false"; |
624 | break; |
625 | case 1: |
626 | $name = "true"; |
627 | break; |
628 | default: |
629 | $name = "unknown"; |
630 | } |
631 | return $name; |
632 | } |
633 | |
6e557c08 |
634 | /** |
c81415c7 |
635 | * generates <text></text> tags, processing raw text therein |
6e557c08 |
636 | * @param int ilev the current indent level |
637 | * @param boolean short stick it on one line |
638 | * @return string formatted text |
c81415c7 |
639 | */ |
84769fd8 |
640 | function writetext( $raw, $ilev=0, $short=true) { |
84769fd8 |
641 | $indent = str_repeat( " ",$ilev ); |
642 | |
643 | // encode the text to 'disguise' HTML content |
644 | $raw = htmlspecialchars( $raw ); |
645 | |
646 | if ($short) { |
647 | $xml = "$indent<text>$raw</text>\n"; |
648 | } |
649 | else { |
650 | $xml = "$indent<text>\n$raw\n$indent</text>\n"; |
651 | } |
652 | |
653 | return $xml; |
654 | } |
655 | |
656 | function presave_process( $content ) { |
657 | // override method to allow us to add xml headers and footers |
658 | |
659 | $content = "<?xml version=\"1.0\"?>\n" . |
660 | "<quiz>\n" . |
661 | $content . "\n" . |
662 | "</quiz>"; |
663 | |
664 | return $content; |
665 | } |
666 | |
6e557c08 |
667 | /** |
c81415c7 |
668 | * Include an image encoded in base 64 |
6e557c08 |
669 | * @param string imagepath The location of the image file |
670 | * @return string xml code segment |
c81415c7 |
671 | */ |
d08e16b2 |
672 | function writeimage( $imagepath ) { |
d08e16b2 |
673 | global $CFG; |
674 | |
675 | if (empty($imagepath)) { |
676 | return ''; |
677 | } |
678 | |
679 | $courseid = $this->course->id; |
680 | if (!$binary = file_get_contents( "{$CFG->dataroot}/$courseid/$imagepath" )) { |
681 | return ''; |
682 | } |
683 | |
684 | $content = " <image_base64>\n".addslashes(base64_encode( $binary ))."\n". |
685 | "\n </image_base64>\n"; |
686 | return $content; |
687 | } |
688 | |
6e557c08 |
689 | /** |
c81415c7 |
690 | * Turns question into an xml segment |
6e557c08 |
691 | * @param array question question array |
692 | * @return string xml segment |
c81415c7 |
693 | */ |
84769fd8 |
694 | function writequestion( $question ) { |
84769fd8 |
695 | // initial string; |
696 | $expout = ""; |
697 | |
698 | // add comment |
699 | $expout .= "\n\n<!-- question: $question->id -->\n"; |
700 | |
701 | // add opening tag |
f1abd39f |
702 | // generates specific header for Cloze and category type question |
703 | if ($question->qtype == 'category') { |
704 | $expout .= " <question type=\"category\">\n"; |
705 | $expout .= " <category>\n"; |
706 | $expout .= " $question->category\n"; |
707 | $expout .= " </category>\n"; |
708 | $expout .= " </question>\n"; |
709 | return $expout; |
710 | } |
711 | elseif ($question->qtype != MULTIANSWER) { |
7b8bc256 |
712 | // for all question types except Close |
713 | $question_type = $this->get_qtype( $question->qtype ); |
714 | $name_text = $this->writetext( $question->name ); |
715 | $qtformat = $this->get_format($question->questiontextformat); |
716 | $question_text = $this->writetext( $question->questiontext ); |
a4514d91 |
717 | $generalfeedback = $this->writetext( $question->generalfeedback ); |
7b8bc256 |
718 | $expout .= " <question type=\"$question_type\">\n"; |
719 | $expout .= " <name>$name_text</name>\n"; |
720 | $expout .= " <questiontext format=\"$qtformat\">\n"; |
721 | $expout .= $question_text; |
722 | $expout .= " </questiontext>\n"; |
723 | $expout .= " <image>{$question->image}</image>\n"; |
724 | $expout .= $this->writeimage($question->image); |
a4514d91 |
725 | $expout .= " <generalfeedback>\n"; |
726 | $expout .= $generalfeedback; |
727 | $expout .= " </generalfeedback>\n"; |
c81415c7 |
728 | $expout .= " <defaultgrade>{$question->defaultgrade}</defaultgrade>\n"; |
7b8bc256 |
729 | $expout .= " <penalty>{$question->penalty}</penalty>\n"; |
730 | $expout .= " <hidden>{$question->hidden}</hidden>\n"; |
731 | } |
732 | else { |
733 | // for Cloze type only |
734 | $question_type = $this->get_qtype( $question->qtype ); |
735 | $name_text = $this->writetext( $question->name ); |
736 | $question_text = $this->writetext( $question->questiontext ); |
737 | $expout .= " <question type=\"$question_type\">\n"; |
738 | $expout .= " <name>$name_text</name>\n"; |
739 | $expout .= " <questiontext>\n"; |
740 | $expout .= $question_text; |
741 | $expout .= " </questiontext>\n"; |
742 | } |
743 | |
d08e16b2 |
744 | if (!empty($question->options->shuffleanswers)) { |
745 | $expout .= " <shuffleanswers>{$question->options->shuffleanswers}</shuffleanswers>\n"; |
746 | } |
747 | else { |
748 | $expout .= " <shuffleanswers>0</shuffleanswers>\n"; |
749 | } |
84769fd8 |
750 | |
751 | // output depends on question type |
752 | switch($question->qtype) { |
f1abd39f |
753 | case 'category': |
754 | // not a qtype really - dummy used for category switching |
755 | break; |
84769fd8 |
756 | case TRUEFALSE: |
36e2232e |
757 | foreach ($question->options->answers as $answer) { |
758 | $fraction_pc = round( $answer->fraction * 100 ); |
3246ed33 |
759 | if ($answer->id == $question->options->trueanswer) { |
760 | $answertext = 'true'; |
761 | } else { |
762 | $answertext = 'false'; |
763 | } |
36e2232e |
764 | $expout .= " <answer fraction=\"$fraction_pc\">\n"; |
3246ed33 |
765 | $expout .= $this->writetext($answertext, 3) . "\n"; |
36e2232e |
766 | $expout .= " <feedback>\n"; |
767 | $expout .= $this->writetext( $answer->feedback,4,false ); |
768 | $expout .= " </feedback>\n"; |
769 | $expout .= " </answer>\n"; |
770 | } |
84769fd8 |
771 | break; |
772 | case MULTICHOICE: |
773 | $expout .= " <single>".$this->get_single($question->options->single)."</single>\n"; |
307f045f |
774 | $expout .= " <shuffleanswers>".$this->get_single($question->options->shuffleanswers)."</shuffleanswers>\n"; |
775 | $expout .= " <correctfeedback>".$this->writetext($question->options->correctfeedback, 3)."</correctfeedback>\n"; |
776 | $expout .= " <partiallycorrectfeedback>".$this->writetext($question->options->partiallycorrectfeedback, 3)."</partiallycorrectfeedback>\n"; |
777 | $expout .= " <incorrectfeedback>".$this->writetext($question->options->incorrectfeedback, 3)."</incorrectfeedback>\n"; |
84769fd8 |
778 | foreach($question->options->answers as $answer) { |
779 | $percent = $answer->fraction * 100; |
780 | $expout .= " <answer fraction=\"$percent\">\n"; |
781 | $expout .= $this->writetext( $answer->answer,4,false ); |
782 | $expout .= " <feedback>\n"; |
6e557c08 |
783 | $expout .= $this->writetext( $answer->feedback,5,false ); |
84769fd8 |
784 | $expout .= " </feedback>\n"; |
785 | $expout .= " </answer>\n"; |
786 | } |
787 | break; |
788 | case SHORTANSWER: |
789 | $expout .= " <usecase>{$question->options->usecase}</usecase>\n "; |
790 | foreach($question->options->answers as $answer) { |
791 | $percent = 100 * $answer->fraction; |
792 | $expout .= " <answer fraction=\"$percent\">\n"; |
793 | $expout .= $this->writetext( $answer->answer,3,false ); |
794 | $expout .= " <feedback>\n"; |
795 | $expout .= $this->writetext( $answer->feedback,4,false ); |
796 | $expout .= " </feedback>\n"; |
797 | $expout .= " </answer>\n"; |
798 | } |
799 | break; |
7b8bc256 |
800 | //case regexp: |
801 | //$expout .= " <usecase>{$question->options->usecase}</usecase>\n "; |
802 | // foreach($question->options->answers as $answer) { |
803 | // $percent = 100 * $answer->fraction; |
804 | // $expout .= " <answer fraction=\"$percent\">\n"; |
805 | // $expout .= $this->writetext( $answer->answer,3,false ); |
806 | // $expout .= " <feedback>\n"; |
807 | // $expout .= $this->writetext( $answer->feedback,4,false ); |
808 | // $expout .= " </feedback>\n"; |
809 | // $expout .= " </answer>\n"; |
810 | // } |
811 | // break; |
84769fd8 |
812 | case NUMERICAL: |
813 | foreach ($question->options->answers as $answer) { |
814 | $tolerance = $answer->tolerance; |
55c54868 |
815 | $percent = 100 * $answer->fraction; |
816 | $expout .= "<answer fraction=\"$percent\">\n"; |
817 | // <text> tags are an added feature, old filed won't have them |
818 | $expout .= " <text>{$answer->answer}</text>\n"; |
84769fd8 |
819 | $expout .= " <tolerance>$tolerance</tolerance>\n"; |
820 | $expout .= " <feedback>".$this->writetext( $answer->feedback )."</feedback>\n"; |
55c54868 |
821 | // fraction tag is deprecated |
822 | // $expout .= " <fraction>{$answer->fraction}</fraction>\n"; |
84769fd8 |
823 | $expout .= "</answer>\n"; |
824 | } |
825 | |
826 | $units = $question->options->units; |
827 | if (count($units)) { |
828 | $expout .= "<units>\n"; |
829 | foreach ($units as $unit) { |
830 | $expout .= " <unit>\n"; |
831 | $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n"; |
832 | $expout .= " <unit_name>{$unit->unit}</unit_name>\n"; |
833 | $expout .= " </unit>\n"; |
834 | } |
835 | $expout .= "</units>\n"; |
836 | } |
837 | break; |
838 | case MATCH: |
839 | foreach($question->options->subquestions as $subquestion) { |
840 | $expout .= "<subquestion>\n"; |
841 | $expout .= $this->writetext( $subquestion->questiontext ); |
842 | $expout .= "<answer>".$this->writetext( $subquestion->answertext )."</answer>\n"; |
843 | $expout .= "</subquestion>\n"; |
844 | } |
845 | break; |
846 | case DESCRIPTION: |
c81415c7 |
847 | // nothing more to do for this type |
84769fd8 |
848 | break; |
849 | case MULTIANSWER: |
7b8bc256 |
850 | $a_count=1; |
851 | foreach($question->options->questions as $question) { |
852 | $thispattern = addslashes("{#".$a_count."}"); |
853 | $thisreplace = $question->questiontext; |
854 | $expout=ereg_replace($thispattern, $thisreplace, $expout ); |
855 | $a_count++; |
856 | } |
857 | break; |
c81415c7 |
858 | case ESSAY: |
859 | foreach ($question->options->answers as $answer) { |
55c54868 |
860 | $percent = 100 * $answer->fraction; |
861 | $expout .= "<answer fraction=\"$percent\">\n"; |
c81415c7 |
862 | $expout .= " <feedback>".$this->writetext( $answer->feedback )."</feedback>\n"; |
55c54868 |
863 | // fraction tag is deprecated |
864 | // $expout .= " <fraction>{$answer->fraction}</fraction>\n"; |
c81415c7 |
865 | $expout .= "</answer>\n"; |
866 | } |
867 | |
868 | break; |
84769fd8 |
869 | default: |
870 | $expout .= "<!-- Question type is unknown or not supported (Type=$question->qtype) -->\n"; |
871 | } |
872 | |
873 | // close the question tag |
874 | $expout .= "</question>\n"; |
875 | |
84769fd8 |
876 | return $expout; |
877 | } |
878 | } |
879 | |
880 | ?> |