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