84769fd8 |
1 | <?php // $Id$ |
2 | // |
3 | /////////////////////////////////////////////////////////////// |
4 | // XML import/export |
5 | // |
6 | ////////////////////////////////////////////////////////////////////////// |
7 | // Based on default.php, included by ../import.php |
8 | |
9 | |
10 | require_once( "$CFG->libdir/xmlize.php" ); |
11 | |
f5565b69 |
12 | class qformat_xml extends qformat_default { |
84769fd8 |
13 | |
14 | function provide_import() { |
15 | return true; |
16 | } |
17 | |
18 | function provide_export() { |
19 | return true; |
20 | } |
21 | |
22 | // IMPORT FUNCTIONS START HERE |
23 | |
24 | function trans_format( $name ) { |
25 | // translate text format string to its internal code |
26 | |
27 | $name = trim($name); |
28 | |
29 | if ($name=='moodle_auto_format') { |
30 | $id = 0; |
31 | } |
32 | elseif ($name=='html') { |
33 | $id = 1; |
34 | } |
35 | elseif ($name=='plain_text') { |
36 | $id = 2; |
37 | } |
38 | elseif ($name=='wiki_like') { |
39 | $id = 3; |
40 | } |
41 | elseif ($name=='markdown') { |
42 | $id = 4; |
43 | } |
44 | else { |
45 | $id = 0; // or maybe warning required |
46 | } |
47 | return $id; |
48 | } |
49 | |
50 | function trans_single( $name ) { |
51 | // translate single string to its internal format |
52 | |
53 | $name = trim($name); |
54 | |
55 | if ($name=="true") { |
56 | $id = 1; |
57 | } |
58 | elseif ($name=="false") { |
59 | $id = 0; |
60 | } |
61 | else { |
62 | $id = 0; // or maybe warning required |
63 | } |
64 | return $id; |
65 | } |
66 | |
67 | function import_text( $text ) { |
68 | // handle xml 'text' element |
69 | $data = $text[0]['#']; |
70 | $data = html_entity_decode( $data ); |
71 | return addslashes(trim( $data )); |
72 | } |
73 | |
74 | function import_headers( $question ) { |
75 | // read bits that are common to all questions |
76 | |
77 | // this routine initialises the question object |
78 | $name = $this->import_text( $question['#']['name'][0]['#']['text'] ); |
79 | $qtext = $this->import_text( $question['#']['questiontext'][0]['#']['text'] ); |
80 | $qformat = $question['#']['questiontext'][0]['@']['format']; |
81 | $image = $question['#']['image'][0]['#']; |
d08e16b2 |
82 | if (!empty($question['#']['image_base64'][0]['#'])) { |
83 | $image_base64 = stripslashes( trim( $question['#']['image_base64'][0]['#'] ) ); |
84 | $image = $this->importimagefile( $image, $image_base64 ); |
85 | } |
84769fd8 |
86 | $penalty = $question['#']['penalty'][0]['#']; |
87 | |
51bcdf28 |
88 | $qo = $this->defaultquestion(); |
84769fd8 |
89 | $qo->name = $name; |
90 | $qo->questiontext = $qtext; |
91 | $qo->questiontextformat = $this->trans_format( $qformat ); |
92 | $qo->image = ((!empty($image)) ? $image : ''); |
93 | $qo->penalty = $penalty; |
94 | |
95 | return $qo; |
96 | } |
97 | |
98 | |
99 | function import_answer( $answer ) { |
100 | // import answer part of question |
101 | |
102 | $fraction = $answer['@']['fraction']; |
103 | $text = $this->import_text( $answer['#']['text']); |
104 | $feedback = $this->import_text( $answer['#']['feedback'][0]['#']['text'] ); |
105 | |
106 | $ans = null; |
107 | $ans->answer = $text; |
108 | $ans->fraction = $fraction / 100; |
109 | $ans->feedback = $feedback; |
110 | |
111 | return $ans; |
112 | } |
113 | |
114 | function import_multichoice( $question ) { |
115 | // import multichoice type questions |
116 | |
117 | // get common parts |
118 | $qo = $this->import_headers( $question ); |
119 | |
120 | // 'header' parts particular to multichoice |
121 | $qo->qtype = MULTICHOICE; |
122 | $single = $question['#']['single'][0]['#']; |
123 | $qo->single = $this->trans_single( $single ); |
124 | |
125 | // run through the answers |
126 | $answers = $question['#']['answer']; |
127 | $a_count = 0; |
128 | foreach ($answers as $answer) { |
129 | $ans = $this->import_answer( $answer ); |
130 | $qo->answer[$a_count] = $ans->answer; |
131 | $qo->fraction[$a_count] = $ans->fraction; |
132 | $qo->feedback[$a_count] = $ans->feedback; |
133 | ++$a_count; |
134 | } |
135 | |
136 | return $qo; |
137 | } |
138 | |
139 | function import_truefalse( $question ) { |
140 | // import true/false type question |
141 | |
142 | // get common parts |
143 | $qo = $this->import_headers( $question ); |
144 | |
145 | // 'header' parts particular to true/false |
146 | $qo->qtype = TRUEFALSE; |
147 | |
148 | // get answer info |
149 | $answers = $question['#']['answer']; |
150 | $fraction0 = $answers[0]['@']['fraction']; |
151 | $feedback0 = $this->import_text($answers[0]['#']['feedback'][0]['#']['text']); |
152 | $fraction1 = $answers[1]['@']['fraction']; |
153 | $feedback1 = $this->import_text($answers[1]['#']['feedback'][0]['#']['text']); |
154 | |
155 | // sort out which is true and build object accordingly |
156 | if ($fraction0==100) { // then 0 index is true |
157 | $qo->answer = 1; |
158 | $qo->feedbacktrue=$feedback0; |
159 | $qo->feedbackfalse=$feedback1; |
160 | } |
161 | else { |
162 | $qo->answer = 0; |
163 | $qo->feedbacktrue = $feedback1; |
164 | $qo->feedbackfalse = $feedback0; |
165 | } |
166 | |
167 | return $qo; |
168 | } |
169 | |
170 | function import_shortanswer( $question ) { |
171 | // import short answer question |
172 | |
173 | // get common parts |
174 | $qo = $this->import_headers( $question ); |
175 | |
176 | // header parts particular to shortanswer |
177 | $qo->qtype = SHORTANSWER; |
178 | |
179 | // get usecase |
180 | $qo->usecase = $question['#']['usecase'][0]['#']; |
181 | |
182 | // run through the answers |
183 | $answers = $question['#']['answer']; |
184 | $a_count = 0; |
185 | foreach ($answers as $answer) { |
186 | $ans = $this->import_answer( $answer ); |
187 | $qo->answer[$a_count] = $ans->answer; |
188 | $qo->fraction[$a_count] = $ans->fraction; |
189 | $qo->feedback[$a_count] = $ans->feedback; |
190 | ++$a_count; |
191 | } |
192 | |
193 | return $qo; |
194 | } |
195 | |
196 | function import_numerical( $question ) { |
197 | // import numerical question |
198 | |
199 | // get common parts |
200 | $qo = $this->import_headers( $question ); |
201 | |
202 | // header parts particular to numerical |
203 | $qo->qtype = NUMERICAL; |
204 | |
205 | // get answers array |
206 | $answers = $question['#']['answer']; |
207 | $qo->answer = array(); |
208 | $qo->feedback = array(); |
209 | $qo->fraction = array(); |
210 | $qo->tolerance = array(); |
211 | foreach ($answers as $answer) { |
212 | $qo->answer[] = $answer['#'][0]; |
a0d187bf |
213 | $qo->feedback[] = $this->import_text( $answer['#']['feedback'][0]['#']['text'] ); |
84769fd8 |
214 | $qo->fraction[] = $answer['#']['fraction'][0]['#']; |
215 | $qo->tolerance[] = $answer['#']['tolerance'][0]['#']; |
216 | } |
217 | |
218 | // get units array |
84769fd8 |
219 | $qo->unit = array(); |
a0d187bf |
220 | if (isset($question['#']['units'][0]['#']['unit'])) { |
221 | $units = $question['#']['units'][0]['#']['unit']; |
222 | $qo->multiplier = array(); |
223 | foreach ($units as $unit) { |
224 | $qo->multiplier[] = $unit['#']['multiplier'][0]['#']; |
225 | $qo->unit[] = $unit['#']['unit_name'][0]['#']; |
226 | } |
84769fd8 |
227 | } |
84769fd8 |
228 | return $qo; |
229 | } |
230 | |
51bcdf28 |
231 | function import_matching( $question ) { |
232 | // import matching question |
233 | |
234 | // get common parts |
235 | $qo = $this->import_headers( $question ); |
236 | |
237 | // header parts particular to matching |
238 | $qo->qtype = MATCH; |
239 | $qo->shuffleanswers = $question['#']['shuffleanswers'][0]['#']; |
240 | |
241 | // get subquestions |
242 | $subquestions = $question['#']['subquestion']; |
243 | $qo->subquestions = array(); |
244 | $qo->subanswers = array(); |
245 | |
246 | // run through subquestions |
247 | foreach ($subquestions as $subquestion) { |
a0d187bf |
248 | $qtext = $this->import_text( $subquestion['#']['text'] ); |
249 | $atext = $this->import_text( $subquestion['#']['answer'][0]['#']['text'] ); |
51bcdf28 |
250 | $qo->subquestions[] = $qtext; |
251 | $qo->subanswers[] = $atext; |
252 | } |
51bcdf28 |
253 | return $qo; |
254 | } |
255 | |
84769fd8 |
256 | function readquestions($lines) { |
257 | // parse the array of lines into an array of questions |
258 | // this *could* burn memory - but it won't happen that much |
259 | // so fingers crossed! |
260 | |
261 | // we just need it as one big string |
262 | $text = implode($lines, " "); |
263 | unset( $lines ); |
264 | |
265 | // this converts xml to big nasty data structure |
266 | // the 0 means keep white space as it is (important for markdown format) |
267 | // print_r it if you want to see what it looks like! |
268 | $xml = xmlize( $text, 0 ); |
269 | |
270 | // set up array to hold all our questions |
271 | $questions = array(); |
272 | |
273 | // iterate through questions |
274 | foreach ($xml['quiz']['#']['question'] as $question) { |
275 | $question_type = $question['@']['type']; |
276 | $questiontype = get_string( 'questiontype','quiz',$question_type ); |
c515fcf7 |
277 | // echo "<p>$questiontype</p>"; |
84769fd8 |
278 | |
279 | if ($question_type=='multichoice') { |
280 | $qo = $this->import_multichoice( $question ); |
281 | } |
282 | elseif ($question_type=='truefalse') { |
283 | $qo = $this->import_truefalse( $question ); |
284 | } |
285 | elseif ($question_type=='shortanswer') { |
286 | $qo = $this->import_shortanswer( $question ); |
287 | } |
288 | elseif ($question_type=='numerical') { |
289 | $qo = $this->import_numerical( $question ); |
290 | } |
51bcdf28 |
291 | elseif ($question_type=='matching') { |
292 | $qo = $this->import_matching( $question ); |
293 | } |
84769fd8 |
294 | else { |
295 | $notsupported = get_string( 'xmlnotsupported','quiz',$question_type ); |
296 | echo "<p>$notsupported</p>"; |
297 | $qo = null; |
298 | } |
299 | |
300 | // stick the result in the $questions array |
301 | if ($qo) { |
302 | $questions[] = $qo; |
303 | } |
304 | } |
305 | |
306 | return $questions; |
307 | } |
308 | |
309 | // EXPORT FUNCTIONS START HERE |
310 | |
311 | function indent_xhtml($source, $indenter = ' ') { |
312 | // xml tidier-upper |
313 | // (c) Ari Koivula http://ventionline.com |
314 | |
315 | // Remove all pre-existing formatting. |
316 | // Remove all newlines. |
317 | $source = str_replace("\n", '', $source); |
318 | $source = str_replace("\r", '', $source); |
319 | // Remove all tabs. |
320 | $source = str_replace("\t", '', $source); |
321 | // Remove all space after ">" and before "<". |
322 | $source = ereg_replace(">( )*", ">", $source); |
323 | $source = ereg_replace("( )*<", "<", $source); |
324 | |
325 | // Iterate through the source. |
326 | $level = 0; |
327 | $source_len = strlen($source); |
328 | $pt = 0; |
329 | while ($pt < $source_len) { |
330 | if ($source{$pt} === '<') { |
331 | // We have entered a tag. |
332 | // Remember the point where the tag starts. |
333 | $started_at = $pt; |
334 | $tag_level = 1; |
335 | // If the second letter of the tag is "/", assume its an ending tag. |
336 | if ($source{$pt+1} === '/') { |
337 | $tag_level = -1; |
338 | } |
339 | // If the second letter of the tag is "!", assume its an "invisible" tag. |
340 | if ($source{$pt+1} === '!') { |
341 | $tag_level = 0; |
342 | } |
343 | // Iterate throught the source until the end of tag. |
344 | while ($source{$pt} !== '>') { |
345 | $pt++; |
346 | } |
347 | // If the second last letter is "/", assume its a self ending tag. |
348 | if ($source{$pt-1} === '/') { |
349 | $tag_level = 0; |
350 | } |
351 | $tag_lenght = $pt+1-$started_at; |
352 | |
353 | // Decide the level of indention for this tag. |
354 | // If this was an ending tag, decrease indent level for this tag.. |
355 | if ($tag_level === -1) { |
356 | $level--; |
357 | } |
358 | // Place the tag in an array with proper indention. |
359 | $array[] = str_repeat($indenter, $level).substr($source, $started_at, $tag_lenght); |
360 | // If this was a starting tag, increase the indent level after this tag. |
361 | if ($tag_level === 1) { |
362 | $level++; |
363 | } |
364 | // if it was a self closing tag, dont do shit. |
365 | } |
366 | // Were out of the tag. |
367 | // If next letter exists... |
368 | if (($pt+1) < $source_len) { |
369 | // ... and its not an "<". |
370 | if ($source{$pt+1} !== '<') { |
371 | $started_at = $pt+1; |
372 | // Iterate through the source until the start of new tag or until we reach the end of file. |
373 | while ($source{$pt} !== '<' && $pt < $source_len) { |
374 | $pt++; |
375 | } |
376 | // If we found a "<" (we didnt find the end of file) |
377 | if ($source{$pt} === '<') { |
378 | $tag_lenght = $pt-$started_at; |
379 | // Place the stuff in an array with proper indention. |
380 | $array[] = str_repeat($indenter, $level).substr($source, $started_at, $tag_lenght); |
381 | } |
382 | // If the next tag is "<", just advance pointer and let the tag indenter take care of it. |
383 | } else { |
384 | $pt++; |
385 | } |
386 | // If the next letter doesnt exist... Were done... well, almost.. |
387 | } else { |
388 | break; |
389 | } |
390 | } |
391 | // Replace old source with the new one we just collected into our array. |
392 | $source = implode($array, "\n"); |
393 | return $source; |
394 | } |
395 | |
396 | |
397 | function export_file_extension() { |
398 | // override default type so extension is .xml |
399 | |
400 | return ".xml"; |
401 | } |
402 | |
403 | function get_qtype( $type_id ) { |
404 | // translates question type code number into actual name |
405 | |
406 | switch( $type_id ) { |
407 | case TRUEFALSE: |
408 | $name = 'truefalse'; |
409 | break; |
410 | case MULTICHOICE: |
411 | $name = 'multichoice'; |
412 | break; |
413 | case SHORTANSWER: |
414 | $name = 'shortanswer'; |
415 | break; |
416 | case NUMERICAL: |
417 | $name = 'numerical'; |
418 | break; |
419 | case MATCH: |
420 | $name = 'matching'; |
421 | break; |
422 | case DESCRIPTION: |
423 | $name = 'description'; |
424 | break; |
425 | case MULTIANSWER: |
426 | $name = 'cloze'; |
427 | break; |
428 | default: |
429 | $name = 'unknown'; |
430 | } |
431 | return $name; |
432 | } |
433 | |
434 | function get_format( $id ) { |
435 | // translates question text format id number into something sensible |
436 | |
437 | switch( $id ) { |
438 | case 0: |
439 | $name = "moodle_auto_format"; |
440 | break; |
441 | case 1: |
442 | $name = "html"; |
443 | break; |
444 | case 2: |
445 | $name = "plain_text"; |
446 | break; |
447 | case 3: |
448 | $name = "wiki_like"; |
449 | break; |
450 | case 4: |
451 | $name = "markdown"; |
452 | break; |
453 | default: |
454 | $name = "unknown"; |
455 | } |
456 | return $name; |
457 | } |
458 | |
459 | function get_single( $id ) { |
460 | // translate single value into something sensible |
461 | |
462 | switch( $id ) { |
463 | case 0: |
464 | $name = "false"; |
465 | break; |
466 | case 1: |
467 | $name = "true"; |
468 | break; |
469 | default: |
470 | $name = "unknown"; |
471 | } |
472 | return $name; |
473 | } |
474 | |
475 | function writetext( $raw, $ilev=0, $short=true) { |
476 | // generates <text></text> tags, processing raw text therein |
477 | // $ilev is the current indent level |
478 | // $short=true sticks it on one line |
479 | $indent = str_repeat( " ",$ilev ); |
480 | |
481 | // encode the text to 'disguise' HTML content |
482 | $raw = htmlspecialchars( $raw ); |
483 | |
484 | if ($short) { |
485 | $xml = "$indent<text>$raw</text>\n"; |
486 | } |
487 | else { |
488 | $xml = "$indent<text>\n$raw\n$indent</text>\n"; |
489 | } |
490 | |
491 | return $xml; |
492 | } |
493 | |
494 | function presave_process( $content ) { |
495 | // override method to allow us to add xml headers and footers |
496 | |
497 | $content = "<?xml version=\"1.0\"?>\n" . |
498 | "<quiz>\n" . |
499 | $content . "\n" . |
500 | "</quiz>"; |
501 | |
502 | return $content; |
503 | } |
504 | |
d08e16b2 |
505 | function writeimage( $imagepath ) { |
506 | // includes image in base64 |
507 | global $CFG; |
508 | |
509 | if (empty($imagepath)) { |
510 | return ''; |
511 | } |
512 | |
513 | $courseid = $this->course->id; |
514 | if (!$binary = file_get_contents( "{$CFG->dataroot}/$courseid/$imagepath" )) { |
515 | return ''; |
516 | } |
517 | |
518 | $content = " <image_base64>\n".addslashes(base64_encode( $binary ))."\n". |
519 | "\n </image_base64>\n"; |
520 | return $content; |
521 | } |
522 | |
84769fd8 |
523 | function writequestion( $question ) { |
524 | // turns question into string |
525 | // question reflects database fields for general question and specific to type |
526 | |
527 | // initial string; |
528 | $expout = ""; |
529 | |
530 | // add comment |
531 | $expout .= "\n\n<!-- question: $question->id -->\n"; |
532 | |
533 | // add opening tag |
534 | $question_type = $this->get_qtype( $question->qtype ); |
535 | $name_text = $this->writetext( $question->name ); |
536 | $qtformat = $this->get_format($question->questiontextformat); |
537 | $question_text = $this->writetext( $question->questiontext ); |
538 | $expout .= " <question type=\"$question_type\">\n"; |
539 | $expout .= " <name>$name_text</name>\n"; |
540 | $expout .= " <questiontext format=\"$qtformat\">\n"; |
541 | $expout .= $question_text; |
542 | $expout .= " </questiontext>\n"; |
d08e16b2 |
543 | $expout .= " <image>{$question->image}</image>\n"; |
544 | $expout .= $this->writeimage($question->image); |
84769fd8 |
545 | $expout .= " <penalty>{$question->penalty}</penalty>\n"; |
546 | $expout .= " <hidden>{$question->hidden}</hidden>\n"; |
d08e16b2 |
547 | if (!empty($question->options->shuffleanswers)) { |
548 | $expout .= " <shuffleanswers>{$question->options->shuffleanswers}</shuffleanswers>\n"; |
549 | } |
550 | else { |
551 | $expout .= " <shuffleanswers>0</shuffleanswers>\n"; |
552 | } |
84769fd8 |
553 | |
554 | // output depends on question type |
555 | switch($question->qtype) { |
556 | case TRUEFALSE: |
36e2232e |
557 | foreach ($question->options->answers as $answer) { |
558 | $fraction_pc = round( $answer->fraction * 100 ); |
559 | $expout .= " <answer fraction=\"$fraction_pc\">\n"; |
560 | $expout .= $this->writetext(strtolower($answer->answer),3)."\n"; |
561 | $expout .= " <feedback>\n"; |
562 | $expout .= $this->writetext( $answer->feedback,4,false ); |
563 | $expout .= " </feedback>\n"; |
564 | $expout .= " </answer>\n"; |
565 | } |
84769fd8 |
566 | break; |
567 | case MULTICHOICE: |
568 | $expout .= " <single>".$this->get_single($question->options->single)."</single>\n"; |
569 | foreach($question->options->answers as $answer) { |
570 | $percent = $answer->fraction * 100; |
571 | $expout .= " <answer fraction=\"$percent\">\n"; |
572 | $expout .= $this->writetext( $answer->answer,4,false ); |
573 | $expout .= " <feedback>\n"; |
574 | $expout .= $this->writetext( $answer->feedback,4,false ); |
575 | $expout .= " </feedback>\n"; |
576 | $expout .= " </answer>\n"; |
577 | } |
578 | break; |
579 | case SHORTANSWER: |
580 | $expout .= " <usecase>{$question->options->usecase}</usecase>\n "; |
581 | foreach($question->options->answers as $answer) { |
582 | $percent = 100 * $answer->fraction; |
583 | $expout .= " <answer fraction=\"$percent\">\n"; |
584 | $expout .= $this->writetext( $answer->answer,3,false ); |
585 | $expout .= " <feedback>\n"; |
586 | $expout .= $this->writetext( $answer->feedback,4,false ); |
587 | $expout .= " </feedback>\n"; |
588 | $expout .= " </answer>\n"; |
589 | } |
590 | break; |
591 | case NUMERICAL: |
592 | foreach ($question->options->answers as $answer) { |
593 | $tolerance = $answer->tolerance; |
594 | $expout .= "<answer>\n"; |
595 | $expout .= " {$answer->answer}\n"; |
596 | $expout .= " <tolerance>$tolerance</tolerance>\n"; |
597 | $expout .= " <feedback>".$this->writetext( $answer->feedback )."</feedback>\n"; |
598 | $expout .= " <fraction>{$answer->fraction}</fraction>\n"; |
599 | $expout .= "</answer>\n"; |
600 | } |
601 | |
602 | $units = $question->options->units; |
603 | if (count($units)) { |
604 | $expout .= "<units>\n"; |
605 | foreach ($units as $unit) { |
606 | $expout .= " <unit>\n"; |
607 | $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n"; |
608 | $expout .= " <unit_name>{$unit->unit}</unit_name>\n"; |
609 | $expout .= " </unit>\n"; |
610 | } |
611 | $expout .= "</units>\n"; |
612 | } |
613 | break; |
614 | case MATCH: |
615 | foreach($question->options->subquestions as $subquestion) { |
616 | $expout .= "<subquestion>\n"; |
617 | $expout .= $this->writetext( $subquestion->questiontext ); |
618 | $expout .= "<answer>".$this->writetext( $subquestion->answertext )."</answer>\n"; |
619 | $expout .= "</subquestion>\n"; |
620 | } |
621 | break; |
622 | case DESCRIPTION: |
623 | // nothing more to do for this type |
624 | break; |
625 | case MULTIANSWER: |
626 | $expout .= "<!-- CLOZE type is not supported -->\n"; |
627 | break; |
628 | default: |
629 | $expout .= "<!-- Question type is unknown or not supported (Type=$question->qtype) -->\n"; |
630 | } |
631 | |
632 | // close the question tag |
633 | $expout .= "</question>\n"; |
634 | |
635 | // run through xml tidy function |
636 | // $tidy_expout = $this->indent_xhtml( $expout, ' ' ) . "\n\n"; |
637 | |
638 | return $expout; |
639 | } |
640 | } |
641 | |
642 | ?> |