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