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