[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Code for exporting questions as Moodle XML. 19 * 20 * @package qformat_xml 21 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once($CFG->libdir . '/xmlize.php'); 29 if (!class_exists('qformat_default')) { 30 // This is ugly, but this class is also (ab)used by mod/lesson, which defines 31 // a different base class in mod/lesson/format.php. Thefore, we can only 32 // include the proper base class conditionally like this. (We have to include 33 // the base class like this, otherwise it breaks third-party question types.) 34 // This may be reviewd, and a better fix found one day. 35 require_once($CFG->dirroot . '/question/format.php'); 36 } 37 38 39 /** 40 * Importer for Moodle XML question format. 41 * 42 * See http://docs.moodle.org/en/Moodle_XML_format for a description of the format. 43 * 44 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 45 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 46 */ 47 class qformat_xml extends qformat_default { 48 49 public function provide_import() { 50 return true; 51 } 52 53 public function provide_export() { 54 return true; 55 } 56 57 public function mime_type() { 58 return 'application/xml'; 59 } 60 61 // IMPORT FUNCTIONS START HERE. 62 63 /** 64 * Translate human readable format name 65 * into internal Moodle code number 66 * @param string name format name from xml file 67 * @return int Moodle format code 68 */ 69 public function trans_format($name) { 70 $name = trim($name); 71 72 if ($name == 'moodle_auto_format') { 73 return FORMAT_MOODLE; 74 } else if ($name == 'html') { 75 return FORMAT_HTML; 76 } else if ($name == 'plain_text') { 77 return FORMAT_PLAIN; 78 } else if ($name == 'wiki_like') { 79 return FORMAT_WIKI; 80 } else if ($name == 'markdown') { 81 return FORMAT_MARKDOWN; 82 } else { 83 debugging("Unrecognised text format '{$name}' in the import file. Assuming 'html'."); 84 return FORMAT_HTML; 85 } 86 } 87 88 /** 89 * Translate human readable single answer option 90 * to internal code number 91 * @param string name true/false 92 * @return int internal code number 93 */ 94 public function trans_single($name) { 95 $name = trim($name); 96 if ($name == "false" || !$name) { 97 return 0; 98 } else { 99 return 1; 100 } 101 } 102 103 /** 104 * process text string from xml file 105 * @param array $text bit of xml tree after ['text'] 106 * @return string processed text. 107 */ 108 public function import_text($text) { 109 // Quick sanity check. 110 if (empty($text)) { 111 return ''; 112 } 113 $data = $text[0]['#']; 114 return trim($data); 115 } 116 117 /** 118 * return the value of a node, given a path to the node 119 * if it doesn't exist return the default value 120 * @param array xml data to read 121 * @param array path path to node expressed as array 122 * @param mixed default 123 * @param bool istext process as text 124 * @param string error if set value must exist, return false and issue message if not 125 * @return mixed value 126 */ 127 public function getpath($xml, $path, $default, $istext=false, $error='') { 128 foreach ($path as $index) { 129 if (!isset($xml[$index])) { 130 if (!empty($error)) { 131 $this->error($error); 132 return false; 133 } else { 134 return $default; 135 } 136 } 137 138 $xml = $xml[$index]; 139 } 140 141 if ($istext) { 142 if (!is_string($xml)) { 143 $this->error(get_string('invalidxml', 'qformat_xml')); 144 } 145 $xml = trim($xml); 146 } 147 148 return $xml; 149 } 150 151 public function import_text_with_files($data, $path, $defaultvalue = '', $defaultformat = 'html') { 152 $field = array(); 153 $field['text'] = $this->getpath($data, 154 array_merge($path, array('#', 'text', 0, '#')), $defaultvalue, true); 155 $field['format'] = $this->trans_format($this->getpath($data, 156 array_merge($path, array('@', 'format')), $defaultformat)); 157 $itemid = $this->import_files_as_draft($this->getpath($data, 158 array_merge($path, array('#', 'file')), array(), false)); 159 if (!empty($itemid)) { 160 $field['itemid'] = $itemid; 161 } 162 return $field; 163 } 164 165 public function import_files_as_draft($xml) { 166 global $USER; 167 if (empty($xml)) { 168 return null; 169 } 170 $fs = get_file_storage(); 171 $itemid = file_get_unused_draft_itemid(); 172 $filepaths = array(); 173 foreach ($xml as $file) { 174 $filename = $this->getpath($file, array('@', 'name'), '', true); 175 $filepath = $this->getpath($file, array('@', 'path'), '/', true); 176 $fullpath = $filepath . $filename; 177 if (in_array($fullpath, $filepaths)) { 178 debugging('Duplicate file in XML: ' . $fullpath, DEBUG_DEVELOPER); 179 continue; 180 } 181 $filerecord = array( 182 'contextid' => context_user::instance($USER->id)->id, 183 'component' => 'user', 184 'filearea' => 'draft', 185 'itemid' => $itemid, 186 'filepath' => $filepath, 187 'filename' => $filename, 188 ); 189 $fs->create_file_from_string($filerecord, base64_decode($file['#'])); 190 $filepaths[] = $fullpath; 191 } 192 return $itemid; 193 } 194 195 /** 196 * import parts of question common to all types 197 * @param $question array question question array from xml tree 198 * @return object question object 199 */ 200 public function import_headers($question) { 201 global $USER; 202 203 // This routine initialises the question object. 204 $qo = $this->defaultquestion(); 205 206 // Question name. 207 $qo->name = $this->clean_question_name($this->getpath($question, 208 array('#', 'name', 0, '#', 'text', 0, '#'), '', true, 209 get_string('xmlimportnoname', 'qformat_xml'))); 210 $questiontext = $this->import_text_with_files($question, 211 array('#', 'questiontext', 0)); 212 $qo->questiontext = $questiontext['text']; 213 $qo->questiontextformat = $questiontext['format']; 214 if (!empty($questiontext['itemid'])) { 215 $qo->questiontextitemid = $questiontext['itemid']; 216 } 217 // Backwards compatibility, deal with the old image tag. 218 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false); 219 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false); 220 if ($filedata && $filename) { 221 $fs = get_file_storage(); 222 if (empty($qo->questiontextitemid)) { 223 $qo->questiontextitemid = file_get_unused_draft_itemid(); 224 } 225 $filename = clean_param(str_replace('/', '_', $filename), PARAM_FILE); 226 $filerecord = array( 227 'contextid' => context_user::instance($USER->id)->id, 228 'component' => 'user', 229 'filearea' => 'draft', 230 'itemid' => $qo->questiontextitemid, 231 'filepath' => '/', 232 'filename' => $filename, 233 ); 234 $fs->create_file_from_string($filerecord, base64_decode($filedata)); 235 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />'; 236 } 237 238 // Restore files in generalfeedback. 239 $generalfeedback = $this->import_text_with_files($question, 240 array('#', 'generalfeedback', 0), $qo->generalfeedback, $this->get_format($qo->questiontextformat)); 241 $qo->generalfeedback = $generalfeedback['text']; 242 $qo->generalfeedbackformat = $generalfeedback['format']; 243 if (!empty($generalfeedback['itemid'])) { 244 $qo->generalfeedbackitemid = $generalfeedback['itemid']; 245 } 246 247 $qo->defaultmark = $this->getpath($question, 248 array('#', 'defaultgrade', 0, '#'), $qo->defaultmark); 249 $qo->penalty = $this->getpath($question, 250 array('#', 'penalty', 0, '#'), $qo->penalty); 251 252 // Fix problematic rounding from old files. 253 if (abs($qo->penalty - 0.3333333) < 0.005) { 254 $qo->penalty = 0.3333333; 255 } 256 257 // Read the question tags. 258 $this->import_question_tags($qo, $question); 259 260 return $qo; 261 } 262 263 /** 264 * Import the common parts of a single answer 265 * @param array answer xml tree for single answer 266 * @param bool $withanswerfiles if true, the answers are HTML (or $defaultformat) 267 * and so may contain files, otherwise the answers are plain text. 268 * @param array Default text format for the feedback, and the answers if $withanswerfiles 269 * is true. 270 * @return object answer object 271 */ 272 public function import_answer($answer, $withanswerfiles = false, $defaultformat = 'html') { 273 $ans = new stdClass(); 274 275 if ($withanswerfiles) { 276 $ans->answer = $this->import_text_with_files($answer, array(), '', $defaultformat); 277 } else { 278 $ans->answer = array(); 279 $ans->answer['text'] = $this->getpath($answer, array('#', 'text', 0, '#'), '', true); 280 $ans->answer['format'] = FORMAT_PLAIN; 281 } 282 283 $ans->feedback = $this->import_text_with_files($answer, array('#', 'feedback', 0), '', $defaultformat); 284 285 $ans->fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100; 286 287 return $ans; 288 } 289 290 /** 291 * Import the common overall feedback fields. 292 * @param object $question the part of the XML relating to this question. 293 * @param object $qo the question data to add the fields to. 294 * @param bool $withshownumpartscorrect include the shownumcorrect field. 295 */ 296 public function import_combined_feedback($qo, $questionxml, $withshownumpartscorrect = false) { 297 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'); 298 foreach ($fields as $field) { 299 $qo->$field = $this->import_text_with_files($questionxml, 300 array('#', $field, 0), '', $this->get_format($qo->questiontextformat)); 301 } 302 303 if ($withshownumpartscorrect) { 304 $qo->shownumcorrect = array_key_exists('shownumcorrect', $questionxml['#']); 305 306 // Backwards compatibility. 307 if (array_key_exists('correctresponsesfeedback', $questionxml['#'])) { 308 $qo->shownumcorrect = $this->trans_single($this->getpath($questionxml, 309 array('#', 'correctresponsesfeedback', 0, '#'), 1)); 310 } 311 } 312 } 313 314 /** 315 * Import a question hint 316 * @param array $hintxml hint xml fragment. 317 * @param string $defaultformat the text format to assume for hints that do not specify. 318 * @return object hint for storing in the database. 319 */ 320 public function import_hint($hintxml, $defaultformat) { 321 $hint = new stdClass(); 322 if (array_key_exists('hintcontent', $hintxml['#'])) { 323 // Backwards compatibility. 324 325 $hint->hint = $this->import_text_with_files($hintxml, 326 array('#', 'hintcontent', 0), '', $defaultformat); 327 328 $hint->shownumcorrect = $this->getpath($hintxml, 329 array('#', 'statenumberofcorrectresponses', 0, '#'), 0); 330 $hint->clearwrong = $this->getpath($hintxml, 331 array('#', 'clearincorrectresponses', 0, '#'), 0); 332 $hint->options = $this->getpath($hintxml, 333 array('#', 'showfeedbacktoresponses', 0, '#'), 0); 334 335 return $hint; 336 } 337 $hint->hint = $this->import_text_with_files($hintxml, array(), '', $defaultformat); 338 $hint->shownumcorrect = array_key_exists('shownumcorrect', $hintxml['#']); 339 $hint->clearwrong = array_key_exists('clearwrong', $hintxml['#']); 340 $hint->options = $this->getpath($hintxml, array('#', 'options', 0, '#'), '', true); 341 342 return $hint; 343 } 344 345 /** 346 * Import all the question hints 347 * 348 * @param object $qo the question data that is being constructed. 349 * @param array $questionxml The xml representing the question. 350 * @param bool $withparts whether the extra fields relating to parts should be imported. 351 * @param bool $withoptions whether the extra options field should be imported. 352 * @param string $defaultformat the text format to assume for hints that do not specify. 353 * @return array of objects representing the hints in the file. 354 */ 355 public function import_hints($qo, $questionxml, $withparts = false, 356 $withoptions = false, $defaultformat = 'html') { 357 if (!isset($questionxml['#']['hint'])) { 358 return; 359 } 360 361 foreach ($questionxml['#']['hint'] as $hintxml) { 362 $hint = $this->import_hint($hintxml, $defaultformat); 363 $qo->hint[] = $hint->hint; 364 365 if ($withparts) { 366 $qo->hintshownumcorrect[] = $hint->shownumcorrect; 367 $qo->hintclearwrong[] = $hint->clearwrong; 368 } 369 370 if ($withoptions) { 371 $qo->hintoptions[] = $hint->options; 372 } 373 } 374 } 375 376 /** 377 * Import all the question tags 378 * 379 * @param object $qo the question data that is being constructed. 380 * @param array $questionxml The xml representing the question. 381 * @return array of objects representing the tags in the file. 382 */ 383 public function import_question_tags($qo, $questionxml) { 384 global $CFG; 385 386 if (core_tag_tag::is_enabled('core_question', 'question') 387 && array_key_exists('tags', $questionxml['#']) 388 && !empty($questionxml['#']['tags'][0]['#']['tag'])) { 389 $qo->tags = array(); 390 foreach ($questionxml['#']['tags'][0]['#']['tag'] as $tagdata) { 391 $qo->tags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true); 392 } 393 } 394 } 395 396 /** 397 * Import files from a node in the XML. 398 * @param array $xml an array of <file> nodes from the the parsed XML. 399 * @return array of things representing files - in the form that save_question expects. 400 */ 401 public function import_files($xml) { 402 $files = array(); 403 foreach ($xml as $file) { 404 $data = new stdClass(); 405 $data->content = $file['#']; 406 $data->encoding = $file['@']['encoding']; 407 $data->name = $file['@']['name']; 408 $files[] = $data; 409 } 410 return $files; 411 } 412 413 /** 414 * import multiple choice question 415 * @param array question question array from xml tree 416 * @return object question object 417 */ 418 public function import_multichoice($question) { 419 // Get common parts. 420 $qo = $this->import_headers($question); 421 422 // Header parts particular to multichoice. 423 $qo->qtype = 'multichoice'; 424 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true'); 425 $qo->single = $this->trans_single($single); 426 $shuffleanswers = $this->getpath($question, 427 array('#', 'shuffleanswers', 0, '#'), 'false'); 428 $qo->answernumbering = $this->getpath($question, 429 array('#', 'answernumbering', 0, '#'), 'abc'); 430 $qo->shuffleanswers = $this->trans_single($shuffleanswers); 431 432 // There was a time on the 1.8 branch when it could output an empty 433 // answernumbering tag, so fix up any found. 434 if (empty($qo->answernumbering)) { 435 $qo->answernumbering = 'abc'; 436 } 437 438 // Run through the answers. 439 $answers = $question['#']['answer']; 440 $acount = 0; 441 foreach ($answers as $answer) { 442 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat)); 443 $qo->answer[$acount] = $ans->answer; 444 $qo->fraction[$acount] = $ans->fraction; 445 $qo->feedback[$acount] = $ans->feedback; 446 ++$acount; 447 } 448 449 $this->import_combined_feedback($qo, $question, true); 450 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat)); 451 452 return $qo; 453 } 454 455 /** 456 * Import cloze type question 457 * @param array question question array from xml tree 458 * @return object question object 459 */ 460 public function import_multianswer($question) { 461 global $USER; 462 question_bank::get_qtype('multianswer'); 463 464 $questiontext = $this->import_text_with_files($question, 465 array('#', 'questiontext', 0)); 466 $qo = qtype_multianswer_extract_question($questiontext); 467 468 // Header parts particular to multianswer. 469 $qo->qtype = 'multianswer'; 470 471 // Only set the course if the data is available. 472 if (isset($this->course)) { 473 $qo->course = $this->course; 474 } 475 476 $qo->name = $this->clean_question_name($this->import_text($question['#']['name'][0]['#']['text'])); 477 $qo->questiontextformat = $questiontext['format']; 478 $qo->questiontext = $qo->questiontext['text']; 479 if (!empty($questiontext['itemid'])) { 480 $qo->questiontextitemid = $questiontext['itemid']; 481 } 482 483 // Backwards compatibility, deal with the old image tag. 484 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false); 485 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false); 486 if ($filedata && $filename) { 487 $fs = get_file_storage(); 488 if (empty($qo->questiontextitemid)) { 489 $qo->questiontextitemid = file_get_unused_draft_itemid(); 490 } 491 $filename = clean_param(str_replace('/', '_', $filename), PARAM_FILE); 492 $filerecord = array( 493 'contextid' => context_user::instance($USER->id)->id, 494 'component' => 'user', 495 'filearea' => 'draft', 496 'itemid' => $qo->questiontextitemid, 497 'filepath' => '/', 498 'filename' => $filename, 499 ); 500 $fs->create_file_from_string($filerecord, base64_decode($filedata)); 501 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />'; 502 } 503 504 // Restore files in generalfeedback. 505 $generalfeedback = $this->import_text_with_files($question, 506 array('#', 'generalfeedback', 0), $qo->generalfeedback, $this->get_format($qo->questiontextformat)); 507 $qo->generalfeedback = $generalfeedback['text']; 508 $qo->generalfeedbackformat = $generalfeedback['format']; 509 if (!empty($generalfeedback['itemid'])) { 510 $qo->generalfeedbackitemid = $generalfeedback['itemid']; 511 } 512 513 $qo->penalty = $this->getpath($question, 514 array('#', 'penalty', 0, '#'), $this->defaultquestion()->penalty); 515 // Fix problematic rounding from old files. 516 if (abs($qo->penalty - 0.3333333) < 0.005) { 517 $qo->penalty = 0.3333333; 518 } 519 520 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat)); 521 $this->import_question_tags($qo, $question); 522 523 return $qo; 524 } 525 526 /** 527 * Import true/false type question 528 * @param array question question array from xml tree 529 * @return object question object 530 */ 531 public function import_truefalse($question) { 532 // Get common parts. 533 global $OUTPUT; 534 $qo = $this->import_headers($question); 535 536 // Header parts particular to true/false. 537 $qo->qtype = 'truefalse'; 538 539 // In the past, it used to be assumed that the two answers were in the file 540 // true first, then false. Howevever that was not always true. Now, we 541 // try to match on the answer text, but in old exports, this will be a localised 542 // string, so if we don't find true or false, we fall back to the old system. 543 $first = true; 544 $warning = false; 545 foreach ($question['#']['answer'] as $answer) { 546 $answertext = $this->getpath($answer, 547 array('#', 'text', 0, '#'), '', true); 548 $feedback = $this->import_text_with_files($answer, 549 array('#', 'feedback', 0), '', $this->get_format($qo->questiontextformat)); 550 551 if ($answertext != 'true' && $answertext != 'false') { 552 // Old style file, assume order is true/false. 553 $warning = true; 554 if ($first) { 555 $answertext = 'true'; 556 } else { 557 $answertext = 'false'; 558 } 559 } 560 561 if ($answertext == 'true') { 562 $qo->answer = ($answer['@']['fraction'] == 100); 563 $qo->correctanswer = $qo->answer; 564 $qo->feedbacktrue = $feedback; 565 } else { 566 $qo->answer = ($answer['@']['fraction'] != 100); 567 $qo->correctanswer = $qo->answer; 568 $qo->feedbackfalse = $feedback; 569 } 570 $first = false; 571 } 572 573 if ($warning) { 574 $a = new stdClass(); 575 $a->questiontext = $qo->questiontext; 576 $a->answer = get_string($qo->correctanswer ? 'true' : 'false', 'qtype_truefalse'); 577 echo $OUTPUT->notification(get_string('truefalseimporterror', 'qformat_xml', $a)); 578 } 579 580 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat)); 581 582 return $qo; 583 } 584 585 /** 586 * Import short answer type question 587 * @param array question question array from xml tree 588 * @return object question object 589 */ 590 public function import_shortanswer($question) { 591 // Get common parts. 592 $qo = $this->import_headers($question); 593 594 // Header parts particular to shortanswer. 595 $qo->qtype = 'shortanswer'; 596 597 // Get usecase. 598 $qo->usecase = $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase); 599 600 // Run through the answers. 601 $answers = $question['#']['answer']; 602 $acount = 0; 603 foreach ($answers as $answer) { 604 $ans = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat)); 605 $qo->answer[$acount] = $ans->answer['text']; 606 $qo->fraction[$acount] = $ans->fraction; 607 $qo->feedback[$acount] = $ans->feedback; 608 ++$acount; 609 } 610 611 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat)); 612 613 return $qo; 614 } 615 616 /** 617 * Import description type question 618 * @param array question question array from xml tree 619 * @return object question object 620 */ 621 public function import_description($question) { 622 // Get common parts. 623 $qo = $this->import_headers($question); 624 // Header parts particular to shortanswer. 625 $qo->qtype = 'description'; 626 $qo->defaultmark = 0; 627 $qo->length = 0; 628 return $qo; 629 } 630 631 /** 632 * Import numerical type question 633 * @param array question question array from xml tree 634 * @return object question object 635 */ 636 public function import_numerical($question) { 637 // Get common parts. 638 $qo = $this->import_headers($question); 639 640 // Header parts particular to numerical. 641 $qo->qtype = 'numerical'; 642 643 // Get answers array. 644 $answers = $question['#']['answer']; 645 $qo->answer = array(); 646 $qo->feedback = array(); 647 $qo->fraction = array(); 648 $qo->tolerance = array(); 649 foreach ($answers as $answer) { 650 // Answer outside of <text> is deprecated. 651 $obj = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat)); 652 $qo->answer[] = $obj->answer['text']; 653 if (empty($qo->answer)) { 654 $qo->answer = '*'; 655 } 656 $qo->feedback[] = $obj->feedback; 657 $qo->tolerance[] = $this->getpath($answer, array('#', 'tolerance', 0, '#'), 0); 658 659 // Fraction as a tag is deprecated. 660 $fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100; 661 $qo->fraction[] = $this->getpath($answer, 662 array('#', 'fraction', 0, '#'), $fraction); // Deprecated. 663 } 664 665 // Get the units array. 666 $qo->unit = array(); 667 $units = $this->getpath($question, array('#', 'units', 0, '#', 'unit'), array()); 668 if (!empty($units)) { 669 $qo->multiplier = array(); 670 foreach ($units as $unit) { 671 $qo->multiplier[] = $this->getpath($unit, array('#', 'multiplier', 0, '#'), 1); 672 $qo->unit[] = $this->getpath($unit, array('#', 'unit_name', 0, '#'), '', true); 673 } 674 } 675 $qo->unitgradingtype = $this->getpath($question, array('#', 'unitgradingtype', 0, '#'), 0); 676 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0.1); 677 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), null); 678 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0); 679 $qo->instructions['text'] = ''; 680 $qo->instructions['format'] = FORMAT_HTML; 681 $instructions = $this->getpath($question, array('#', 'instructions'), array()); 682 if (!empty($instructions)) { 683 $qo->instructions = $this->import_text_with_files($instructions, 684 array('0'), '', $this->get_format($qo->questiontextformat)); 685 } 686 687 if (is_null($qo->showunits)) { 688 // Set a good default, depending on whether there are any units defined. 689 if (empty($qo->unit)) { 690 $qo->showunits = 3; // This is qtype_numerical::UNITNONE, but we cannot refer to that constant here. 691 } else { 692 $qo->showunits = 0; // This is qtype_numerical::UNITOPTIONAL, but we cannot refer to that constant here. 693 } 694 } 695 696 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat)); 697 698 return $qo; 699 } 700 701 /** 702 * Import matching type question 703 * @param array question question array from xml tree 704 * @return object question object 705 */ 706 public function import_match($question) { 707 // Get common parts. 708 $qo = $this->import_headers($question); 709 710 // Header parts particular to matching. 711 $qo->qtype = 'match'; 712 $qo->shuffleanswers = $this->trans_single($this->getpath($question, 713 array('#', 'shuffleanswers', 0, '#'), 1)); 714 715 // Run through subquestions. 716 $qo->subquestions = array(); 717 $qo->subanswers = array(); 718 foreach ($question['#']['subquestion'] as $subqxml) { 719 $qo->subquestions[] = $this->import_text_with_files($subqxml, 720 array(), '', $this->get_format($qo->questiontextformat)); 721 722 $answers = $this->getpath($subqxml, array('#', 'answer'), array()); 723 $qo->subanswers[] = $this->getpath($subqxml, 724 array('#', 'answer', 0, '#', 'text', 0, '#'), '', true); 725 } 726 727 $this->import_combined_feedback($qo, $question, true); 728 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat)); 729 730 return $qo; 731 } 732 733 /** 734 * Import essay type question 735 * @param array question question array from xml tree 736 * @return object question object 737 */ 738 public function import_essay($question) { 739 // Get common parts. 740 $qo = $this->import_headers($question); 741 742 // Header parts particular to essay. 743 $qo->qtype = 'essay'; 744 745 $qo->responseformat = $this->getpath($question, 746 array('#', 'responseformat', 0, '#'), 'editor'); 747 $qo->responsefieldlines = $this->getpath($question, 748 array('#', 'responsefieldlines', 0, '#'), 15); 749 $qo->responserequired = $this->getpath($question, 750 array('#', 'responserequired', 0, '#'), 1); 751 $qo->attachments = $this->getpath($question, 752 array('#', 'attachments', 0, '#'), 0); 753 $qo->attachmentsrequired = $this->getpath($question, 754 array('#', 'attachmentsrequired', 0, '#'), 0); 755 $qo->graderinfo = $this->import_text_with_files($question, 756 array('#', 'graderinfo', 0), '', $this->get_format($qo->questiontextformat)); 757 $qo->responsetemplate['text'] = $this->getpath($question, 758 array('#', 'responsetemplate', 0, '#', 'text', 0, '#'), '', true); 759 $qo->responsetemplate['format'] = $this->trans_format($this->getpath($question, 760 array('#', 'responsetemplate', 0, '@', 'format'), $this->get_format($qo->questiontextformat))); 761 762 return $qo; 763 } 764 765 /** 766 * Import a calculated question 767 * @param object $question the imported XML data. 768 */ 769 public function import_calculated($question) { 770 771 // Get common parts. 772 $qo = $this->import_headers($question); 773 774 // Header parts particular to calculated. 775 $qo->qtype = 'calculated'; 776 $qo->synchronize = $this->getpath($question, array('#', 'synchronize', 0, '#'), 0); 777 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true'); 778 $qo->single = $this->trans_single($single); 779 $shuffleanswers = $this->getpath($question, array('#', 'shuffleanswers', 0, '#'), 'false'); 780 $qo->answernumbering = $this->getpath($question, 781 array('#', 'answernumbering', 0, '#'), 'abc'); 782 $qo->shuffleanswers = $this->trans_single($shuffleanswers); 783 784 $this->import_combined_feedback($qo, $question); 785 786 $qo->unitgradingtype = $this->getpath($question, 787 array('#', 'unitgradingtype', 0, '#'), 0); 788 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), null); 789 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0); 790 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0); 791 $qo->instructions = $this->getpath($question, 792 array('#', 'instructions', 0, '#', 'text', 0, '#'), '', true); 793 if (!empty($instructions)) { 794 $qo->instructions = $this->import_text_with_files($instructions, 795 array('0'), '', $this->get_format($qo->questiontextformat)); 796 } 797 798 // Get answers array. 799 $answers = $question['#']['answer']; 800 $qo->answer = array(); 801 $qo->feedback = array(); 802 $qo->fraction = array(); 803 $qo->tolerance = array(); 804 $qo->tolerancetype = array(); 805 $qo->correctanswerformat = array(); 806 $qo->correctanswerlength = array(); 807 $qo->feedback = array(); 808 foreach ($answers as $answer) { 809 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat)); 810 // Answer outside of <text> is deprecated. 811 if (empty($ans->answer['text'])) { 812 $ans->answer['text'] = '*'; 813 } 814 $qo->answer[] = $ans->answer['text']; 815 $qo->feedback[] = $ans->feedback; 816 $qo->tolerance[] = $answer['#']['tolerance'][0]['#']; 817 // Fraction as a tag is deprecated. 818 if (!empty($answer['#']['fraction'][0]['#'])) { 819 $qo->fraction[] = $answer['#']['fraction'][0]['#']; 820 } else { 821 $qo->fraction[] = $answer['@']['fraction'] / 100; 822 } 823 $qo->tolerancetype[] = $answer['#']['tolerancetype'][0]['#']; 824 $qo->correctanswerformat[] = $answer['#']['correctanswerformat'][0]['#']; 825 $qo->correctanswerlength[] = $answer['#']['correctanswerlength'][0]['#']; 826 } 827 // Get units array. 828 $qo->unit = array(); 829 if (isset($question['#']['units'][0]['#']['unit'])) { 830 $units = $question['#']['units'][0]['#']['unit']; 831 $qo->multiplier = array(); 832 foreach ($units as $unit) { 833 $qo->multiplier[] = $unit['#']['multiplier'][0]['#']; 834 $qo->unit[] = $unit['#']['unit_name'][0]['#']; 835 } 836 } 837 $instructions = $this->getpath($question, array('#', 'instructions'), array()); 838 if (!empty($instructions)) { 839 $qo->instructions = $this->import_text_with_files($instructions, 840 array('0'), '', $this->get_format($qo->questiontextformat)); 841 } 842 843 if (is_null($qo->unitpenalty)) { 844 // Set a good default, depending on whether there are any units defined. 845 if (empty($qo->unit)) { 846 $qo->showunits = 3; // This is qtype_numerical::UNITNONE, but we cannot refer to that constant here. 847 } else { 848 $qo->showunits = 0; // This is qtype_numerical::UNITOPTIONAL, but we cannot refer to that constant here. 849 } 850 } 851 852 $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition']; 853 $qo->dataset = array(); 854 $qo->datasetindex= 0; 855 foreach ($datasets as $dataset) { 856 $qo->datasetindex++; 857 $qo->dataset[$qo->datasetindex] = new stdClass(); 858 $qo->dataset[$qo->datasetindex]->status = 859 $this->import_text($dataset['#']['status'][0]['#']['text']); 860 $qo->dataset[$qo->datasetindex]->name = 861 $this->import_text($dataset['#']['name'][0]['#']['text']); 862 $qo->dataset[$qo->datasetindex]->type = 863 $dataset['#']['type'][0]['#']; 864 $qo->dataset[$qo->datasetindex]->distribution = 865 $this->import_text($dataset['#']['distribution'][0]['#']['text']); 866 $qo->dataset[$qo->datasetindex]->max = 867 $this->import_text($dataset['#']['maximum'][0]['#']['text']); 868 $qo->dataset[$qo->datasetindex]->min = 869 $this->import_text($dataset['#']['minimum'][0]['#']['text']); 870 $qo->dataset[$qo->datasetindex]->length = 871 $this->import_text($dataset['#']['decimals'][0]['#']['text']); 872 $qo->dataset[$qo->datasetindex]->distribution = 873 $this->import_text($dataset['#']['distribution'][0]['#']['text']); 874 $qo->dataset[$qo->datasetindex]->itemcount = $dataset['#']['itemcount'][0]['#']; 875 $qo->dataset[$qo->datasetindex]->datasetitem = array(); 876 $qo->dataset[$qo->datasetindex]->itemindex = 0; 877 $qo->dataset[$qo->datasetindex]->number_of_items = $this->getpath($dataset, 878 array('#', 'number_of_items', 0, '#'), 0); 879 $datasetitems = $this->getpath($dataset, 880 array('#', 'dataset_items', 0, '#', 'dataset_item'), array()); 881 foreach ($datasetitems as $datasetitem) { 882 $qo->dataset[$qo->datasetindex]->itemindex++; 883 $qo->dataset[$qo->datasetindex]->datasetitem[ 884 $qo->dataset[$qo->datasetindex]->itemindex] = new stdClass(); 885 $qo->dataset[$qo->datasetindex]->datasetitem[ 886 $qo->dataset[$qo->datasetindex]->itemindex]->itemnumber = 887 $datasetitem['#']['number'][0]['#']; 888 $qo->dataset[$qo->datasetindex]->datasetitem[ 889 $qo->dataset[$qo->datasetindex]->itemindex]->value = 890 $datasetitem['#']['value'][0]['#']; 891 } 892 } 893 894 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat)); 895 896 return $qo; 897 } 898 899 /** 900 * This is not a real question type. It's a dummy type used to specify the 901 * import category. The format is: 902 * <question type="category"> 903 * <category>tom/dick/harry</category> 904 * </question> 905 */ 906 protected function import_category($question) { 907 $qo = new stdClass(); 908 $qo->qtype = 'category'; 909 $qo->category = $this->import_text($question['#']['category'][0]['#']['text']); 910 return $qo; 911 } 912 913 /** 914 * Parse the array of lines into an array of questions 915 * this *could* burn memory - but it won't happen that much 916 * so fingers crossed! 917 * @param array of lines from the input file. 918 * @param stdClass $context 919 * @return array (of objects) question objects. 920 */ 921 protected function readquestions($lines) { 922 // We just need it as one big string. 923 $lines = implode('', $lines); 924 925 // This converts xml to big nasty data structure 926 // the 0 means keep white space as it is (important for markdown format). 927 try { 928 $xml = xmlize($lines, 0, 'UTF-8', true); 929 } catch (xml_format_exception $e) { 930 $this->error($e->getMessage(), ''); 931 return false; 932 } 933 unset($lines); // No need to keep this in memory. 934 return $this->import_questions($xml['quiz']['#']['question']); 935 } 936 937 /** 938 * @param array $xml the xmlized xml 939 * @return stdClass[] question objects to pass to question type save_question_options 940 */ 941 public function import_questions($xml) { 942 $questions = array(); 943 944 // Iterate through questions. 945 foreach ($xml as $questionxml) { 946 $qo = $this->import_question($questionxml); 947 948 // Stick the result in the $questions array. 949 if ($qo) { 950 $questions[] = $qo; 951 } 952 } 953 return $questions; 954 } 955 956 /** 957 * @param array $questionxml xml describing the question 958 * @return null|stdClass an object with data to be fed to question type save_question_options 959 */ 960 protected function import_question($questionxml) { 961 $questiontype = $questionxml['@']['type']; 962 963 if ($questiontype == 'multichoice') { 964 return $this->import_multichoice($questionxml); 965 } else if ($questiontype == 'truefalse') { 966 return $this->import_truefalse($questionxml); 967 } else if ($questiontype == 'shortanswer') { 968 return $this->import_shortanswer($questionxml); 969 } else if ($questiontype == 'numerical') { 970 return $this->import_numerical($questionxml); 971 } else if ($questiontype == 'description') { 972 return $this->import_description($questionxml); 973 } else if ($questiontype == 'matching' || $questiontype == 'match') { 974 return $this->import_match($questionxml); 975 } else if ($questiontype == 'cloze' || $questiontype == 'multianswer') { 976 return $this->import_multianswer($questionxml); 977 } else if ($questiontype == 'essay') { 978 return $this->import_essay($questionxml); 979 } else if ($questiontype == 'calculated') { 980 return $this->import_calculated($questionxml); 981 } else if ($questiontype == 'calculatedsimple') { 982 $qo = $this->import_calculated($questionxml); 983 $qo->qtype = 'calculatedsimple'; 984 return $qo; 985 } else if ($questiontype == 'calculatedmulti') { 986 $qo = $this->import_calculated($questionxml); 987 $qo->qtype = 'calculatedmulti'; 988 return $qo; 989 } else if ($questiontype == 'category') { 990 return $this->import_category($questionxml); 991 992 } else { 993 // Not a type we handle ourselves. See if the question type wants 994 // to handle it. 995 if (!$qo = $this->try_importing_using_qtypes($questionxml, null, null, $questiontype)) { 996 $this->error(get_string('xmltypeunsupported', 'qformat_xml', $questiontype)); 997 return null; 998 } 999 return $qo; 1000 } 1001 } 1002 1003 // EXPORT FUNCTIONS START HERE. 1004 1005 public function export_file_extension() { 1006 return '.xml'; 1007 } 1008 1009 /** 1010 * Turn the internal question type name into a human readable form. 1011 * (In the past, the code used to use integers internally. Now, it uses 1012 * strings, so there is less need for this, but to maintain 1013 * backwards-compatibility we change two of the type names.) 1014 * @param string $qtype question type plugin name. 1015 * @return string $qtype string to use in the file. 1016 */ 1017 protected function get_qtype($qtype) { 1018 switch($qtype) { 1019 case 'match': 1020 return 'matching'; 1021 case 'multianswer': 1022 return 'cloze'; 1023 default: 1024 return $qtype; 1025 } 1026 } 1027 1028 /** 1029 * Convert internal Moodle text format code into 1030 * human readable form 1031 * @param int id internal code 1032 * @return string format text 1033 */ 1034 public function get_format($id) { 1035 switch($id) { 1036 case FORMAT_MOODLE: 1037 return 'moodle_auto_format'; 1038 case FORMAT_HTML: 1039 return 'html'; 1040 case FORMAT_PLAIN: 1041 return 'plain_text'; 1042 case FORMAT_WIKI: 1043 return 'wiki_like'; 1044 case FORMAT_MARKDOWN: 1045 return 'markdown'; 1046 default: 1047 return 'unknown'; 1048 } 1049 } 1050 1051 /** 1052 * Convert internal single question code into 1053 * human readable form 1054 * @param int id single question code 1055 * @return string single question string 1056 */ 1057 public function get_single($id) { 1058 switch($id) { 1059 case 0: 1060 return 'false'; 1061 case 1: 1062 return 'true'; 1063 default: 1064 return 'unknown'; 1065 } 1066 } 1067 1068 /** 1069 * Take a string, and wrap it in a CDATA secion, if that is required to make 1070 * the output XML valid. 1071 * @param string $string a string 1072 * @return string the string, wrapped in CDATA if necessary. 1073 */ 1074 public function xml_escape($string) { 1075 if (!empty($string) && htmlspecialchars($string) != $string) { 1076 // If the string contains something that looks like the end 1077 // of a CDATA section, then we need to avoid errors by splitting 1078 // the string between two CDATA sections. 1079 $string = str_replace(']]>', ']]]]><![CDATA[>', $string); 1080 return "<![CDATA[{$string}]]>"; 1081 } else { 1082 return $string; 1083 } 1084 } 1085 1086 /** 1087 * Generates <text></text> tags, processing raw text therein 1088 * @param string $raw the content to output. 1089 * @param int $indent the current indent level. 1090 * @param bool $short stick it on one line. 1091 * @return string formatted text. 1092 */ 1093 public function writetext($raw, $indent = 0, $short = true) { 1094 $indent = str_repeat(' ', $indent); 1095 $raw = $this->xml_escape($raw); 1096 1097 if ($short) { 1098 $xml = "{$indent}<text>{$raw}</text>\n"; 1099 } else { 1100 $xml = "{$indent}<text>\n{$raw}\n{$indent}</text>\n"; 1101 } 1102 1103 return $xml; 1104 } 1105 1106 /** 1107 * Generte the XML to represent some files. 1108 * @param array of store array of stored_file objects. 1109 * @return string $string the XML. 1110 */ 1111 public function write_files($files) { 1112 if (empty($files)) { 1113 return ''; 1114 } 1115 $string = ''; 1116 foreach ($files as $file) { 1117 if ($file->is_directory()) { 1118 continue; 1119 } 1120 $string .= '<file name="' . $file->get_filename() . '" path="' . $file->get_filepath() . '" encoding="base64">'; 1121 $string .= base64_encode($file->get_content()); 1122 $string .= "</file>\n"; 1123 } 1124 return $string; 1125 } 1126 1127 protected function presave_process($content) { 1128 // Override to allow us to add xml headers and footers. 1129 return '<?xml version="1.0" encoding="UTF-8"?> 1130 <quiz> 1131 ' . $content . '</quiz>'; 1132 } 1133 1134 /** 1135 * Turns question into an xml segment 1136 * @param object $question the question data. 1137 * @return string xml segment 1138 */ 1139 public function writequestion($question) { 1140 global $CFG, $OUTPUT; 1141 1142 $invalidquestion = false; 1143 $fs = get_file_storage(); 1144 $contextid = $question->contextid; 1145 // Get files used by the questiontext. 1146 $question->questiontextfiles = $fs->get_area_files( 1147 $contextid, 'question', 'questiontext', $question->id); 1148 // Get files used by the generalfeedback. 1149 $question->generalfeedbackfiles = $fs->get_area_files( 1150 $contextid, 'question', 'generalfeedback', $question->id); 1151 if (!empty($question->options->answers)) { 1152 foreach ($question->options->answers as $answer) { 1153 $answer->answerfiles = $fs->get_area_files( 1154 $contextid, 'question', 'answer', $answer->id); 1155 $answer->feedbackfiles = $fs->get_area_files( 1156 $contextid, 'question', 'answerfeedback', $answer->id); 1157 } 1158 } 1159 1160 $expout = ''; 1161 1162 // Add a comment linking this to the original question id. 1163 $expout .= "<!-- question: {$question->id} -->\n"; 1164 1165 // Check question type. 1166 $questiontype = $this->get_qtype($question->qtype); 1167 1168 // Categories are a special case. 1169 if ($question->qtype == 'category') { 1170 $categorypath = $this->writetext($question->category); 1171 $expout .= " <question type=\"category\">\n"; 1172 $expout .= " <category>\n"; 1173 $expout .= " {$categorypath}\n"; 1174 $expout .= " </category>\n"; 1175 $expout .= " </question>\n"; 1176 return $expout; 1177 } 1178 1179 // Now we know we are are handing a real question. 1180 // Output the generic information. 1181 $expout .= " <question type=\"{$questiontype}\">\n"; 1182 $expout .= " <name>\n"; 1183 $expout .= $this->writetext($question->name, 3); 1184 $expout .= " </name>\n"; 1185 $expout .= " <questiontext {$this->format($question->questiontextformat)}>\n"; 1186 $expout .= $this->writetext($question->questiontext, 3); 1187 $expout .= $this->write_files($question->questiontextfiles); 1188 $expout .= " </questiontext>\n"; 1189 $expout .= " <generalfeedback {$this->format($question->generalfeedbackformat)}>\n"; 1190 $expout .= $this->writetext($question->generalfeedback, 3); 1191 $expout .= $this->write_files($question->generalfeedbackfiles); 1192 $expout .= " </generalfeedback>\n"; 1193 if ($question->qtype != 'multianswer') { 1194 $expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n"; 1195 } 1196 $expout .= " <penalty>{$question->penalty}</penalty>\n"; 1197 $expout .= " <hidden>{$question->hidden}</hidden>\n"; 1198 1199 // The rest of the output depends on question type. 1200 switch($question->qtype) { 1201 case 'category': 1202 // Not a qtype really - dummy used for category switching. 1203 break; 1204 1205 case 'truefalse': 1206 $trueanswer = $question->options->answers[$question->options->trueanswer]; 1207 $trueanswer->answer = 'true'; 1208 $expout .= $this->write_answer($trueanswer); 1209 1210 $falseanswer = $question->options->answers[$question->options->falseanswer]; 1211 $falseanswer->answer = 'false'; 1212 $expout .= $this->write_answer($falseanswer); 1213 break; 1214 1215 case 'multichoice': 1216 $expout .= " <single>" . $this->get_single($question->options->single) . 1217 "</single>\n"; 1218 $expout .= " <shuffleanswers>" . 1219 $this->get_single($question->options->shuffleanswers) . 1220 "</shuffleanswers>\n"; 1221 $expout .= " <answernumbering>" . $question->options->answernumbering . 1222 "</answernumbering>\n"; 1223 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid); 1224 $expout .= $this->write_answers($question->options->answers); 1225 break; 1226 1227 case 'shortanswer': 1228 $expout .= " <usecase>{$question->options->usecase}</usecase>\n"; 1229 $expout .= $this->write_answers($question->options->answers); 1230 break; 1231 1232 case 'numerical': 1233 foreach ($question->options->answers as $answer) { 1234 $expout .= $this->write_answer($answer, 1235 " <tolerance>{$answer->tolerance}</tolerance>\n"); 1236 } 1237 1238 $units = $question->options->units; 1239 if (count($units)) { 1240 $expout .= "<units>\n"; 1241 foreach ($units as $unit) { 1242 $expout .= " <unit>\n"; 1243 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n"; 1244 $expout .= " <unit_name>{$unit->unit}</unit_name>\n"; 1245 $expout .= " </unit>\n"; 1246 } 1247 $expout .= "</units>\n"; 1248 } 1249 if (isset($question->options->unitgradingtype)) { 1250 $expout .= " <unitgradingtype>" . $question->options->unitgradingtype . 1251 "</unitgradingtype>\n"; 1252 } 1253 if (isset($question->options->unitpenalty)) { 1254 $expout .= " <unitpenalty>{$question->options->unitpenalty}</unitpenalty>\n"; 1255 } 1256 if (isset($question->options->showunits)) { 1257 $expout .= " <showunits>{$question->options->showunits}</showunits>\n"; 1258 } 1259 if (isset($question->options->unitsleft)) { 1260 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n"; 1261 } 1262 if (!empty($question->options->instructionsformat)) { 1263 $files = $fs->get_area_files($contextid, 'qtype_numerical', 1264 'instruction', $question->id); 1265 $expout .= " <instructions " . 1266 $this->format($question->options->instructionsformat) . ">\n"; 1267 $expout .= $this->writetext($question->options->instructions, 3); 1268 $expout .= $this->write_files($files); 1269 $expout .= " </instructions>\n"; 1270 } 1271 break; 1272 1273 case 'match': 1274 $expout .= " <shuffleanswers>" . 1275 $this->get_single($question->options->shuffleanswers) . 1276 "</shuffleanswers>\n"; 1277 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid); 1278 foreach ($question->options->subquestions as $subquestion) { 1279 $files = $fs->get_area_files($contextid, 'qtype_match', 1280 'subquestion', $subquestion->id); 1281 $expout .= " <subquestion " . 1282 $this->format($subquestion->questiontextformat) . ">\n"; 1283 $expout .= $this->writetext($subquestion->questiontext, 3); 1284 $expout .= $this->write_files($files); 1285 $expout .= " <answer>\n"; 1286 $expout .= $this->writetext($subquestion->answertext, 4); 1287 $expout .= " </answer>\n"; 1288 $expout .= " </subquestion>\n"; 1289 } 1290 break; 1291 1292 case 'description': 1293 // Nothing else to do. 1294 break; 1295 1296 case 'multianswer': 1297 foreach ($question->options->questions as $index => $subq) { 1298 $expout = str_replace('{#' . $index . '}', $subq->questiontext, $expout); 1299 } 1300 break; 1301 1302 case 'essay': 1303 $expout .= " <responseformat>" . $question->options->responseformat . 1304 "</responseformat>\n"; 1305 $expout .= " <responserequired>" . $question->options->responserequired . 1306 "</responserequired>\n"; 1307 $expout .= " <responsefieldlines>" . $question->options->responsefieldlines . 1308 "</responsefieldlines>\n"; 1309 $expout .= " <attachments>" . $question->options->attachments . 1310 "</attachments>\n"; 1311 $expout .= " <attachmentsrequired>" . $question->options->attachmentsrequired . 1312 "</attachmentsrequired>\n"; 1313 $expout .= " <graderinfo " . 1314 $this->format($question->options->graderinfoformat) . ">\n"; 1315 $expout .= $this->writetext($question->options->graderinfo, 3); 1316 $expout .= $this->write_files($fs->get_area_files($contextid, 'qtype_essay', 1317 'graderinfo', $question->id)); 1318 $expout .= " </graderinfo>\n"; 1319 $expout .= " <responsetemplate " . 1320 $this->format($question->options->responsetemplateformat) . ">\n"; 1321 $expout .= $this->writetext($question->options->responsetemplate, 3); 1322 $expout .= " </responsetemplate>\n"; 1323 break; 1324 1325 case 'calculated': 1326 case 'calculatedsimple': 1327 case 'calculatedmulti': 1328 $expout .= " <synchronize>{$question->options->synchronize}</synchronize>\n"; 1329 $expout .= " <single>{$question->options->single}</single>\n"; 1330 $expout .= " <answernumbering>" . $question->options->answernumbering . 1331 "</answernumbering>\n"; 1332 $expout .= " <shuffleanswers>" . $question->options->shuffleanswers . 1333 "</shuffleanswers>\n"; 1334 1335 $component = 'qtype_' . $question->qtype; 1336 $files = $fs->get_area_files($contextid, $component, 1337 'correctfeedback', $question->id); 1338 $expout .= " <correctfeedback>\n"; 1339 $expout .= $this->writetext($question->options->correctfeedback, 3); 1340 $expout .= $this->write_files($files); 1341 $expout .= " </correctfeedback>\n"; 1342 1343 $files = $fs->get_area_files($contextid, $component, 1344 'partiallycorrectfeedback', $question->id); 1345 $expout .= " <partiallycorrectfeedback>\n"; 1346 $expout .= $this->writetext($question->options->partiallycorrectfeedback, 3); 1347 $expout .= $this->write_files($files); 1348 $expout .= " </partiallycorrectfeedback>\n"; 1349 1350 $files = $fs->get_area_files($contextid, $component, 1351 'incorrectfeedback', $question->id); 1352 $expout .= " <incorrectfeedback>\n"; 1353 $expout .= $this->writetext($question->options->incorrectfeedback, 3); 1354 $expout .= $this->write_files($files); 1355 $expout .= " </incorrectfeedback>\n"; 1356 1357 foreach ($question->options->answers as $answer) { 1358 $percent = 100 * $answer->fraction; 1359 $expout .= "<answer fraction=\"{$percent}\">\n"; 1360 // The "<text/>" tags are an added feature, old files won't have them. 1361 $expout .= " <text>{$answer->answer}</text>\n"; 1362 $expout .= " <tolerance>{$answer->tolerance}</tolerance>\n"; 1363 $expout .= " <tolerancetype>{$answer->tolerancetype}</tolerancetype>\n"; 1364 $expout .= " <correctanswerformat>" . 1365 $answer->correctanswerformat . "</correctanswerformat>\n"; 1366 $expout .= " <correctanswerlength>" . 1367 $answer->correctanswerlength . "</correctanswerlength>\n"; 1368 $expout .= " <feedback {$this->format($answer->feedbackformat)}>\n"; 1369 $files = $fs->get_area_files($contextid, $component, 1370 'instruction', $question->id); 1371 $expout .= $this->writetext($answer->feedback); 1372 $expout .= $this->write_files($answer->feedbackfiles); 1373 $expout .= " </feedback>\n"; 1374 $expout .= "</answer>\n"; 1375 } 1376 if (isset($question->options->unitgradingtype)) { 1377 $expout .= " <unitgradingtype>" . 1378 $question->options->unitgradingtype . "</unitgradingtype>\n"; 1379 } 1380 if (isset($question->options->unitpenalty)) { 1381 $expout .= " <unitpenalty>" . 1382 $question->options->unitpenalty . "</unitpenalty>\n"; 1383 } 1384 if (isset($question->options->showunits)) { 1385 $expout .= " <showunits>{$question->options->showunits}</showunits>\n"; 1386 } 1387 if (isset($question->options->unitsleft)) { 1388 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n"; 1389 } 1390 1391 if (isset($question->options->instructionsformat)) { 1392 $files = $fs->get_area_files($contextid, $component, 1393 'instruction', $question->id); 1394 $expout .= " <instructions " . 1395 $this->format($question->options->instructionsformat) . ">\n"; 1396 $expout .= $this->writetext($question->options->instructions, 3); 1397 $expout .= $this->write_files($files); 1398 $expout .= " </instructions>\n"; 1399 } 1400 1401 if (isset($question->options->units)) { 1402 $units = $question->options->units; 1403 if (count($units)) { 1404 $expout .= "<units>\n"; 1405 foreach ($units as $unit) { 1406 $expout .= " <unit>\n"; 1407 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n"; 1408 $expout .= " <unit_name>{$unit->unit}</unit_name>\n"; 1409 $expout .= " </unit>\n"; 1410 } 1411 $expout .= "</units>\n"; 1412 } 1413 } 1414 1415 // The tag $question->export_process has been set so we get all the 1416 // data items in the database from the function 1417 // qtype_calculated::get_question_options calculatedsimple defaults 1418 // to calculated. 1419 if (isset($question->options->datasets) && count($question->options->datasets)) { 1420 $expout .= "<dataset_definitions>\n"; 1421 foreach ($question->options->datasets as $def) { 1422 $expout .= "<dataset_definition>\n"; 1423 $expout .= " <status>".$this->writetext($def->status)."</status>\n"; 1424 $expout .= " <name>".$this->writetext($def->name)."</name>\n"; 1425 if ($question->qtype == 'calculated') { 1426 $expout .= " <type>calculated</type>\n"; 1427 } else { 1428 $expout .= " <type>calculatedsimple</type>\n"; 1429 } 1430 $expout .= " <distribution>" . $this->writetext($def->distribution) . 1431 "</distribution>\n"; 1432 $expout .= " <minimum>" . $this->writetext($def->minimum) . 1433 "</minimum>\n"; 1434 $expout .= " <maximum>" . $this->writetext($def->maximum) . 1435 "</maximum>\n"; 1436 $expout .= " <decimals>" . $this->writetext($def->decimals) . 1437 "</decimals>\n"; 1438 $expout .= " <itemcount>{$def->itemcount}</itemcount>\n"; 1439 if ($def->itemcount > 0) { 1440 $expout .= " <dataset_items>\n"; 1441 foreach ($def->items as $item) { 1442 $expout .= " <dataset_item>\n"; 1443 $expout .= " <number>".$item->itemnumber."</number>\n"; 1444 $expout .= " <value>".$item->value."</value>\n"; 1445 $expout .= " </dataset_item>\n"; 1446 } 1447 $expout .= " </dataset_items>\n"; 1448 $expout .= " <number_of_items>" . $def->number_of_items . 1449 "</number_of_items>\n"; 1450 } 1451 $expout .= "</dataset_definition>\n"; 1452 } 1453 $expout .= "</dataset_definitions>\n"; 1454 } 1455 break; 1456 1457 default: 1458 // Try support by optional plugin. 1459 if (!$data = $this->try_exporting_using_qtypes($question->qtype, $question)) { 1460 $invalidquestion = true; 1461 } else { 1462 $expout .= $data; 1463 } 1464 } 1465 1466 // Output any hints. 1467 $expout .= $this->write_hints($question); 1468 1469 // Write the question tags. 1470 $tags = core_tag_tag::get_item_tags_array('core_question', 'question', $question->id); 1471 if (!empty($tags)) { 1472 $expout .= " <tags>\n"; 1473 foreach ($tags as $tag) { 1474 $expout .= " <tag>" . $this->writetext($tag, 0, true) . "</tag>\n"; 1475 } 1476 $expout .= " </tags>\n"; 1477 } 1478 1479 // Close the question tag. 1480 $expout .= " </question>\n"; 1481 if ($invalidquestion) { 1482 return ''; 1483 } else { 1484 return $expout; 1485 } 1486 } 1487 1488 public function write_answers($answers) { 1489 if (empty($answers)) { 1490 return; 1491 } 1492 $output = ''; 1493 foreach ($answers as $answer) { 1494 $output .= $this->write_answer($answer); 1495 } 1496 return $output; 1497 } 1498 1499 public function write_answer($answer, $extra = '') { 1500 $percent = $answer->fraction * 100; 1501 $output = ''; 1502 $output .= " <answer fraction=\"{$percent}\" {$this->format($answer->answerformat)}>\n"; 1503 $output .= $this->writetext($answer->answer, 3); 1504 $output .= $this->write_files($answer->answerfiles); 1505 $output .= " <feedback {$this->format($answer->feedbackformat)}>\n"; 1506 $output .= $this->writetext($answer->feedback, 4); 1507 $output .= $this->write_files($answer->feedbackfiles); 1508 $output .= " </feedback>\n"; 1509 $output .= $extra; 1510 $output .= " </answer>\n"; 1511 return $output; 1512 } 1513 1514 /** 1515 * Write out the hints. 1516 * @param object $question the question definition data. 1517 * @return string XML to output. 1518 */ 1519 public function write_hints($question) { 1520 if (empty($question->hints)) { 1521 return ''; 1522 } 1523 1524 $output = ''; 1525 foreach ($question->hints as $hint) { 1526 $output .= $this->write_hint($hint, $question->contextid); 1527 } 1528 return $output; 1529 } 1530 1531 /** 1532 * @param int $format a FORMAT_... constant. 1533 * @return string the attribute to add to an XML tag. 1534 */ 1535 public function format($format) { 1536 return 'format="' . $this->get_format($format) . '"'; 1537 } 1538 1539 public function write_hint($hint, $contextid) { 1540 $fs = get_file_storage(); 1541 $files = $fs->get_area_files($contextid, 'question', 'hint', $hint->id); 1542 1543 $output = ''; 1544 $output .= " <hint {$this->format($hint->hintformat)}>\n"; 1545 $output .= ' ' . $this->writetext($hint->hint); 1546 1547 if (!empty($hint->shownumcorrect)) { 1548 $output .= " <shownumcorrect/>\n"; 1549 } 1550 if (!empty($hint->clearwrong)) { 1551 $output .= " <clearwrong/>\n"; 1552 } 1553 1554 if (!empty($hint->options)) { 1555 $output .= ' <options>' . $this->xml_escape($hint->options) . "</options>\n"; 1556 } 1557 $output .= $this->write_files($files); 1558 $output .= " </hint>\n"; 1559 return $output; 1560 } 1561 1562 /** 1563 * Output the combined feedback fields. 1564 * @param object $questionoptions the question definition data. 1565 * @param int $questionid the question id. 1566 * @param int $contextid the question context id. 1567 * @return string XML to output. 1568 */ 1569 public function write_combined_feedback($questionoptions, $questionid, $contextid) { 1570 $fs = get_file_storage(); 1571 $output = ''; 1572 1573 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'); 1574 foreach ($fields as $field) { 1575 $formatfield = $field . 'format'; 1576 $files = $fs->get_area_files($contextid, 'question', $field, $questionid); 1577 1578 $output .= " <{$field} {$this->format($questionoptions->$formatfield)}>\n"; 1579 $output .= ' ' . $this->writetext($questionoptions->$field); 1580 $output .= $this->write_files($files); 1581 $output .= " </{$field}>\n"; 1582 } 1583 1584 if (!empty($questionoptions->shownumcorrect)) { 1585 $output .= " <shownumcorrect/>\n"; 1586 } 1587 return $output; 1588 } 1589 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |