fix for 5205
[moodle.git] / question / format / xml / format.php
CommitLineData
84769fd8 1<?php // $Id$
2//
3///////////////////////////////////////////////////////////////
4// XML import/export
5//
6//////////////////////////////////////////////////////////////////////////
7// Based on default.php, included by ../import.php
8
9
10require_once( "$CFG->libdir/xmlize.php" );
11
f5565b69 12class 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?>