Moving quiz import/export files to new question area.
[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
12class quiz_format_xml extends quiz_default_format {
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
84 $qo = null;
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
226 function readquestions($lines) {
227 // parse the array of lines into an array of questions
228 // this *could* burn memory - but it won't happen that much
229 // so fingers crossed!
230
231 // we just need it as one big string
232 $text = implode($lines, " ");
233 unset( $lines );
234
235 // this converts xml to big nasty data structure
236 // the 0 means keep white space as it is (important for markdown format)
237 // print_r it if you want to see what it looks like!
238 $xml = xmlize( $text, 0 );
239
240 // set up array to hold all our questions
241 $questions = array();
242
243 // iterate through questions
244 foreach ($xml['quiz']['#']['question'] as $question) {
245 $question_type = $question['@']['type'];
246 $questiontype = get_string( 'questiontype','quiz',$question_type );
247 echo "<p>$questiontype</p>";
248
249 if ($question_type=='multichoice') {
250 $qo = $this->import_multichoice( $question );
251 }
252 elseif ($question_type=='truefalse') {
253 $qo = $this->import_truefalse( $question );
254 }
255 elseif ($question_type=='shortanswer') {
256 $qo = $this->import_shortanswer( $question );
257 }
258 elseif ($question_type=='numerical') {
259 $qo = $this->import_numerical( $question );
260 }
261 else {
262 $notsupported = get_string( 'xmlnotsupported','quiz',$question_type );
263 echo "<p>$notsupported</p>";
264 $qo = null;
265 }
266
267 // stick the result in the $questions array
268 if ($qo) {
269 $questions[] = $qo;
270 }
271 }
272
273 return $questions;
274 }
275
276 // EXPORT FUNCTIONS START HERE
277
278 function indent_xhtml($source, $indenter = ' ') {
279 // xml tidier-upper
280 // (c) Ari Koivula http://ventionline.com
281
282 // Remove all pre-existing formatting.
283 // Remove all newlines.
284 $source = str_replace("\n", '', $source);
285 $source = str_replace("\r", '', $source);
286 // Remove all tabs.
287 $source = str_replace("\t", '', $source);
288 // Remove all space after ">" and before "<".
289 $source = ereg_replace(">( )*", ">", $source);
290 $source = ereg_replace("( )*<", "<", $source);
291
292 // Iterate through the source.
293 $level = 0;
294 $source_len = strlen($source);
295 $pt = 0;
296 while ($pt < $source_len) {
297 if ($source{$pt} === '<') {
298 // We have entered a tag.
299 // Remember the point where the tag starts.
300 $started_at = $pt;
301 $tag_level = 1;
302 // If the second letter of the tag is "/", assume its an ending tag.
303 if ($source{$pt+1} === '/') {
304 $tag_level = -1;
305 }
306 // If the second letter of the tag is "!", assume its an "invisible" tag.
307 if ($source{$pt+1} === '!') {
308 $tag_level = 0;
309 }
310 // Iterate throught the source until the end of tag.
311 while ($source{$pt} !== '>') {
312 $pt++;
313 }
314 // If the second last letter is "/", assume its a self ending tag.
315 if ($source{$pt-1} === '/') {
316 $tag_level = 0;
317 }
318 $tag_lenght = $pt+1-$started_at;
319
320 // Decide the level of indention for this tag.
321 // If this was an ending tag, decrease indent level for this tag..
322 if ($tag_level === -1) {
323 $level--;
324 }
325 // Place the tag in an array with proper indention.
326 $array[] = str_repeat($indenter, $level).substr($source, $started_at, $tag_lenght);
327 // If this was a starting tag, increase the indent level after this tag.
328 if ($tag_level === 1) {
329 $level++;
330 }
331 // if it was a self closing tag, dont do shit.
332 }
333 // Were out of the tag.
334 // If next letter exists...
335 if (($pt+1) < $source_len) {
336 // ... and its not an "<".
337 if ($source{$pt+1} !== '<') {
338 $started_at = $pt+1;
339 // Iterate through the source until the start of new tag or until we reach the end of file.
340 while ($source{$pt} !== '<' && $pt < $source_len) {
341 $pt++;
342 }
343 // If we found a "<" (we didnt find the end of file)
344 if ($source{$pt} === '<') {
345 $tag_lenght = $pt-$started_at;
346 // Place the stuff in an array with proper indention.
347 $array[] = str_repeat($indenter, $level).substr($source, $started_at, $tag_lenght);
348 }
349 // If the next tag is "<", just advance pointer and let the tag indenter take care of it.
350 } else {
351 $pt++;
352 }
353 // If the next letter doesnt exist... Were done... well, almost..
354 } else {
355 break;
356 }
357 }
358 // Replace old source with the new one we just collected into our array.
359 $source = implode($array, "\n");
360 return $source;
361 }
362
363
364 function export_file_extension() {
365 // override default type so extension is .xml
366
367 return ".xml";
368 }
369
370 function get_qtype( $type_id ) {
371 // translates question type code number into actual name
372
373 switch( $type_id ) {
374 case TRUEFALSE:
375 $name = 'truefalse';
376 break;
377 case MULTICHOICE:
378 $name = 'multichoice';
379 break;
380 case SHORTANSWER:
381 $name = 'shortanswer';
382 break;
383 case NUMERICAL:
384 $name = 'numerical';
385 break;
386 case MATCH:
387 $name = 'matching';
388 break;
389 case DESCRIPTION:
390 $name = 'description';
391 break;
392 case MULTIANSWER:
393 $name = 'cloze';
394 break;
395 default:
396 $name = 'unknown';
397 }
398 return $name;
399 }
400
401 function get_format( $id ) {
402 // translates question text format id number into something sensible
403
404 switch( $id ) {
405 case 0:
406 $name = "moodle_auto_format";
407 break;
408 case 1:
409 $name = "html";
410 break;
411 case 2:
412 $name = "plain_text";
413 break;
414 case 3:
415 $name = "wiki_like";
416 break;
417 case 4:
418 $name = "markdown";
419 break;
420 default:
421 $name = "unknown";
422 }
423 return $name;
424 }
425
426 function get_single( $id ) {
427 // translate single value into something sensible
428
429 switch( $id ) {
430 case 0:
431 $name = "false";
432 break;
433 case 1:
434 $name = "true";
435 break;
436 default:
437 $name = "unknown";
438 }
439 return $name;
440 }
441
442 function writetext( $raw, $ilev=0, $short=true) {
443 // generates <text></text> tags, processing raw text therein
444 // $ilev is the current indent level
445 // $short=true sticks it on one line
446 $indent = str_repeat( " ",$ilev );
447
448 // encode the text to 'disguise' HTML content
449 $raw = htmlspecialchars( $raw );
450
451 if ($short) {
452 $xml = "$indent<text>$raw</text>\n";
453 }
454 else {
455 $xml = "$indent<text>\n$raw\n$indent</text>\n";
456 }
457
458 return $xml;
459 }
460
461 function presave_process( $content ) {
462 // override method to allow us to add xml headers and footers
463
464 $content = "<?xml version=\"1.0\"?>\n" .
465 "<quiz>\n" .
466 $content . "\n" .
467 "</quiz>";
468
469 return $content;
470 }
471
472 function writequestion( $question ) {
473 // turns question into string
474 // question reflects database fields for general question and specific to type
475
476 // initial string;
477 $expout = "";
478
479 // add comment
480 $expout .= "\n\n<!-- question: $question->id -->\n";
481
482 // add opening tag
483 $question_type = $this->get_qtype( $question->qtype );
484 $name_text = $this->writetext( $question->name );
485 $qtformat = $this->get_format($question->questiontextformat);
486 $question_text = $this->writetext( $question->questiontext );
487 $expout .= " <question type=\"$question_type\">\n";
488 $expout .= " <name>$name_text</name>\n";
489 $expout .= " <questiontext format=\"$qtformat\">\n";
490 $expout .= $question_text;
491 $expout .= " </questiontext>\n";
492 $expout .= " <image>".$question->image."</image>\n";
493 $expout .= " <penalty>{$question->penalty}</penalty>\n";
494 $expout .= " <hidden>{$question->hidden}</hidden>\n";
495
496 // output depends on question type
497 switch($question->qtype) {
498 case TRUEFALSE:
499 $answer = $question->options->answers;
500 $true_percent = round( $answer['true']->fraction * 100 );
501 $false_percent = round( $answer['false']->fraction * 100 );
502 // true answer
503 $expout .= " <answer fraction=\"$true_percent\">\n";
504 $expout .= $this->writetext("true",3)."\n";
505 $expout .= " <feedback>\n";
506 $expout .= $this->writetext( $answer['true']->feedback,4,false );
507 $expout .= " </feedback>\n";
508 $expout .= " </answer>\n";
509
510 // false answer
511 $expout .= " <answer fraction=\"$false_percent\">\n";
512 $expout .= $this->writetext("false")."\n";
513 $expout .= " <feedback>\n";
514 $expout .= $this->writetext( $answer['false']->feedback,4,false );
515 $expout .= " </feedback>\n";
516 $expout .= " </answer>\n";
517 break;
518 case MULTICHOICE:
519 $expout .= " <single>".$this->get_single($question->options->single)."</single>\n";
520 foreach($question->options->answers as $answer) {
521 $percent = $answer->fraction * 100;
522 $expout .= " <answer fraction=\"$percent\">\n";
523 $expout .= $this->writetext( $answer->answer,4,false );
524 $expout .= " <feedback>\n";
525 $expout .= $this->writetext( $answer->feedback,4,false );
526 $expout .= " </feedback>\n";
527 $expout .= " </answer>\n";
528 }
529 break;
530 case SHORTANSWER:
531 $expout .= " <usecase>{$question->options->usecase}</usecase>\n ";
532 foreach($question->options->answers as $answer) {
533 $percent = 100 * $answer->fraction;
534 $expout .= " <answer fraction=\"$percent\">\n";
535 $expout .= $this->writetext( $answer->answer,3,false );
536 $expout .= " <feedback>\n";
537 $expout .= $this->writetext( $answer->feedback,4,false );
538 $expout .= " </feedback>\n";
539 $expout .= " </answer>\n";
540 }
541 break;
542 case NUMERICAL:
543 foreach ($question->options->answers as $answer) {
544 $tolerance = $answer->tolerance;
545 $expout .= "<answer>\n";
546 $expout .= " {$answer->answer}\n";
547 $expout .= " <tolerance>$tolerance</tolerance>\n";
548 $expout .= " <feedback>".$this->writetext( $answer->feedback )."</feedback>\n";
549 $expout .= " <fraction>{$answer->fraction}</fraction>\n";
550 $expout .= "</answer>\n";
551 }
552
553 $units = $question->options->units;
554 if (count($units)) {
555 $expout .= "<units>\n";
556 foreach ($units as $unit) {
557 $expout .= " <unit>\n";
558 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
559 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
560 $expout .= " </unit>\n";
561 }
562 $expout .= "</units>\n";
563 }
564 break;
565 case MATCH:
566 foreach($question->options->subquestions as $subquestion) {
567 $expout .= "<subquestion>\n";
568 $expout .= $this->writetext( $subquestion->questiontext );
569 $expout .= "<answer>".$this->writetext( $subquestion->answertext )."</answer>\n";
570 $expout .= "</subquestion>\n";
571 }
572 break;
573 case DESCRIPTION:
574 // nothing more to do for this type
575 break;
576 case MULTIANSWER:
577 $expout .= "<!-- CLOZE type is not supported -->\n";
578 break;
579 default:
580 $expout .= "<!-- Question type is unknown or not supported (Type=$question->qtype) -->\n";
581 }
582
583 // close the question tag
584 $expout .= "</question>\n";
585
586 // run through xml tidy function
587 // $tidy_expout = $this->indent_xhtml( $expout, ' ' ) . "\n\n";
588
589 return $expout;
590 }
591}
592
593?>