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