[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Defines the base class for question import and export formats. 19 * 20 * @package moodlecore 21 * @subpackage questionbank 22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 30 /** 31 * Base class for question import and export formats. 32 * 33 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 */ 36 class qformat_default { 37 38 public $displayerrors = true; 39 public $category = null; 40 public $questions = array(); 41 public $course = null; 42 public $filename = ''; 43 public $realfilename = ''; 44 public $matchgrades = 'error'; 45 public $catfromfile = 0; 46 public $contextfromfile = 0; 47 public $cattofile = 0; 48 public $contexttofile = 0; 49 public $questionids = array(); 50 public $importerrors = 0; 51 public $stoponerror = true; 52 public $translator = null; 53 public $canaccessbackupdata = true; 54 55 protected $importcontext = null; 56 57 // functions to indicate import/export functionality 58 // override to return true if implemented 59 60 /** @return bool whether this plugin provides import functionality. */ 61 public function provide_import() { 62 return false; 63 } 64 65 /** @return bool whether this plugin provides export functionality. */ 66 public function provide_export() { 67 return false; 68 } 69 70 /** The string mime-type of the files that this plugin reads or writes. */ 71 public function mime_type() { 72 return mimeinfo('type', $this->export_file_extension()); 73 } 74 75 /** 76 * @return string the file extension (including .) that is normally used for 77 * files handled by this plugin. 78 */ 79 public function export_file_extension() { 80 return '.txt'; 81 } 82 83 /** 84 * Check if the given file is capable of being imported by this plugin. 85 * 86 * Note that expensive or detailed integrity checks on the file should 87 * not be performed by this method. Simple file type or magic-number tests 88 * would be suitable. 89 * 90 * @param stored_file $file the file to check 91 * @return bool whether this plugin can import the file 92 */ 93 public function can_import_file($file) { 94 return ($file->get_mimetype() == $this->mime_type()); 95 } 96 97 // Accessor methods 98 99 /** 100 * set the category 101 * @param object category the category object 102 */ 103 public function setCategory($category) { 104 if (count($this->questions)) { 105 debugging('You shouldn\'t call setCategory after setQuestions'); 106 } 107 $this->category = $category; 108 $this->importcontext = context::instance_by_id($this->category->contextid); 109 } 110 111 /** 112 * Set the specific questions to export. Should not include questions with 113 * parents (sub questions of cloze question type). 114 * Only used for question export. 115 * @param array of question objects 116 */ 117 public function setQuestions($questions) { 118 if ($this->category !== null) { 119 debugging('You shouldn\'t call setQuestions after setCategory'); 120 } 121 $this->questions = $questions; 122 } 123 124 /** 125 * set the course class variable 126 * @param course object Moodle course variable 127 */ 128 public function setCourse($course) { 129 $this->course = $course; 130 } 131 132 /** 133 * set an array of contexts. 134 * @param array $contexts Moodle course variable 135 */ 136 public function setContexts($contexts) { 137 $this->contexts = $contexts; 138 $this->translator = new context_to_string_translator($this->contexts); 139 } 140 141 /** 142 * set the filename 143 * @param string filename name of file to import/export 144 */ 145 public function setFilename($filename) { 146 $this->filename = $filename; 147 } 148 149 /** 150 * set the "real" filename 151 * (this is what the user typed, regardless of wha happened next) 152 * @param string realfilename name of file as typed by user 153 */ 154 public function setRealfilename($realfilename) { 155 $this->realfilename = $realfilename; 156 } 157 158 /** 159 * set matchgrades 160 * @param string matchgrades error or nearest for grades 161 */ 162 public function setMatchgrades($matchgrades) { 163 $this->matchgrades = $matchgrades; 164 } 165 166 /** 167 * set catfromfile 168 * @param bool catfromfile allow categories embedded in import file 169 */ 170 public function setCatfromfile($catfromfile) { 171 $this->catfromfile = $catfromfile; 172 } 173 174 /** 175 * set contextfromfile 176 * @param bool $contextfromfile allow contexts embedded in import file 177 */ 178 public function setContextfromfile($contextfromfile) { 179 $this->contextfromfile = $contextfromfile; 180 } 181 182 /** 183 * set cattofile 184 * @param bool cattofile exports categories within export file 185 */ 186 public function setCattofile($cattofile) { 187 $this->cattofile = $cattofile; 188 } 189 190 /** 191 * set contexttofile 192 * @param bool cattofile exports categories within export file 193 */ 194 public function setContexttofile($contexttofile) { 195 $this->contexttofile = $contexttofile; 196 } 197 198 /** 199 * set stoponerror 200 * @param bool stoponerror stops database write if any errors reported 201 */ 202 public function setStoponerror($stoponerror) { 203 $this->stoponerror = $stoponerror; 204 } 205 206 /** 207 * @param bool $canaccess Whether the current use can access the backup data folder. Determines 208 * where export files are saved. 209 */ 210 public function set_can_access_backupdata($canaccess) { 211 $this->canaccessbackupdata = $canaccess; 212 } 213 214 /*********************** 215 * IMPORTING FUNCTIONS 216 ***********************/ 217 218 /** 219 * Handle parsing error 220 */ 221 protected function error($message, $text='', $questionname='') { 222 $importerrorquestion = get_string('importerrorquestion', 'question'); 223 224 echo "<div class=\"importerror\">\n"; 225 echo "<strong>{$importerrorquestion} {$questionname}</strong>"; 226 if (!empty($text)) { 227 $text = s($text); 228 echo "<blockquote>{$text}</blockquote>\n"; 229 } 230 echo "<strong>{$message}</strong>\n"; 231 echo "</div>"; 232 233 $this->importerrors++; 234 } 235 236 /** 237 * Import for questiontype plugins 238 * Do not override. 239 * @param data mixed The segment of data containing the question 240 * @param question object processed (so far) by standard import code if appropriate 241 * @param extra mixed any additional format specific data that may be passed by the format 242 * @param qtypehint hint about a question type from format 243 * @return object question object suitable for save_options() or false if cannot handle 244 */ 245 public function try_importing_using_qtypes($data, $question = null, $extra = null, 246 $qtypehint = '') { 247 248 // work out what format we are using 249 $formatname = substr(get_class($this), strlen('qformat_')); 250 $methodname = "import_from_{$formatname}"; 251 252 //first try importing using a hint from format 253 if (!empty($qtypehint)) { 254 $qtype = question_bank::get_qtype($qtypehint, false); 255 if (is_object($qtype) && method_exists($qtype, $methodname)) { 256 $question = $qtype->$methodname($data, $question, $this, $extra); 257 if ($question) { 258 return $question; 259 } 260 } 261 } 262 263 // loop through installed questiontypes checking for 264 // function to handle this question 265 foreach (question_bank::get_all_qtypes() as $qtype) { 266 if (method_exists($qtype, $methodname)) { 267 if ($question = $qtype->$methodname($data, $question, $this, $extra)) { 268 return $question; 269 } 270 } 271 } 272 return false; 273 } 274 275 /** 276 * Perform any required pre-processing 277 * @return bool success 278 */ 279 public function importpreprocess() { 280 return true; 281 } 282 283 /** 284 * Process the file 285 * This method should not normally be overidden 286 * @param object $category 287 * @return bool success 288 */ 289 public function importprocess($category) { 290 global $USER, $CFG, $DB, $OUTPUT; 291 292 // Raise time and memory, as importing can be quite intensive. 293 core_php_time_limit::raise(); 294 raise_memory_limit(MEMORY_EXTRA); 295 296 // STAGE 1: Parse the file 297 echo $OUTPUT->notification(get_string('parsingquestions', 'question'), 'notifysuccess'); 298 299 if (! $lines = $this->readdata($this->filename)) { 300 echo $OUTPUT->notification(get_string('cannotread', 'question')); 301 return false; 302 } 303 304 if (!$questions = $this->readquestions($lines)) { // Extract all the questions 305 echo $OUTPUT->notification(get_string('noquestionsinfile', 'question')); 306 return false; 307 } 308 309 // STAGE 2: Write data to database 310 echo $OUTPUT->notification(get_string('importingquestions', 'question', 311 $this->count_questions($questions)), 'notifysuccess'); 312 313 // check for errors before we continue 314 if ($this->stoponerror and ($this->importerrors>0)) { 315 echo $OUTPUT->notification(get_string('importparseerror', 'question')); 316 return true; 317 } 318 319 // get list of valid answer grades 320 $gradeoptionsfull = question_bank::fraction_options_full(); 321 322 // check answer grades are valid 323 // (now need to do this here because of 'stop on error': MDL-10689) 324 $gradeerrors = 0; 325 $goodquestions = array(); 326 foreach ($questions as $question) { 327 if (!empty($question->fraction) and (is_array($question->fraction))) { 328 $fractions = $question->fraction; 329 $invalidfractions = array(); 330 foreach ($fractions as $key => $fraction) { 331 $newfraction = match_grade_options($gradeoptionsfull, $fraction, 332 $this->matchgrades); 333 if ($newfraction === false) { 334 $invalidfractions[] = $fraction; 335 } else { 336 $fractions[$key] = $newfraction; 337 } 338 } 339 if ($invalidfractions) { 340 echo $OUTPUT->notification(get_string('invalidgrade', 'question', 341 implode(', ', $invalidfractions))); 342 ++$gradeerrors; 343 continue; 344 } else { 345 $question->fraction = $fractions; 346 } 347 } 348 $goodquestions[] = $question; 349 } 350 $questions = $goodquestions; 351 352 // check for errors before we continue 353 if ($this->stoponerror && $gradeerrors > 0) { 354 return false; 355 } 356 357 // count number of questions processed 358 $count = 0; 359 360 foreach ($questions as $question) { // Process and store each question 361 $transaction = $DB->start_delegated_transaction(); 362 363 // reset the php timeout 364 core_php_time_limit::raise(); 365 366 // check for category modifiers 367 if ($question->qtype == 'category') { 368 if ($this->catfromfile) { 369 // find/create category object 370 $catpath = $question->category; 371 $newcategory = $this->create_category_path($catpath); 372 if (!empty($newcategory)) { 373 $this->category = $newcategory; 374 } 375 } 376 $transaction->allow_commit(); 377 continue; 378 } 379 $question->context = $this->importcontext; 380 381 $count++; 382 383 echo "<hr /><p><b>{$count}</b>. ".$this->format_question_text($question)."</p>"; 384 385 $question->category = $this->category->id; 386 $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed) 387 388 $question->createdby = $USER->id; 389 $question->timecreated = time(); 390 $question->modifiedby = $USER->id; 391 $question->timemodified = time(); 392 $fileoptions = array( 393 'subdirs' => true, 394 'maxfiles' => -1, 395 'maxbytes' => 0, 396 ); 397 398 $question->id = $DB->insert_record('question', $question); 399 400 if (isset($question->questiontextitemid)) { 401 $question->questiontext = file_save_draft_area_files($question->questiontextitemid, 402 $this->importcontext->id, 'question', 'questiontext', $question->id, 403 $fileoptions, $question->questiontext); 404 } else if (isset($question->questiontextfiles)) { 405 foreach ($question->questiontextfiles as $file) { 406 question_bank::get_qtype($question->qtype)->import_file( 407 $this->importcontext, 'question', 'questiontext', $question->id, $file); 408 } 409 } 410 if (isset($question->generalfeedbackitemid)) { 411 $question->generalfeedback = file_save_draft_area_files($question->generalfeedbackitemid, 412 $this->importcontext->id, 'question', 'generalfeedback', $question->id, 413 $fileoptions, $question->generalfeedback); 414 } else if (isset($question->generalfeedbackfiles)) { 415 foreach ($question->generalfeedbackfiles as $file) { 416 question_bank::get_qtype($question->qtype)->import_file( 417 $this->importcontext, 'question', 'generalfeedback', $question->id, $file); 418 } 419 } 420 $DB->update_record('question', $question); 421 422 $this->questionids[] = $question->id; 423 424 // Now to save all the answers and type-specific options 425 426 $result = question_bank::get_qtype($question->qtype)->save_question_options($question); 427 428 if (isset($question->tags)) { 429 core_tag_tag::set_item_tags('core_question', 'question', $question->id, $question->context, $question->tags); 430 } 431 432 if (!empty($result->error)) { 433 echo $OUTPUT->notification($result->error); 434 // Can't use $transaction->rollback(); since it requires an exception, 435 // and I don't want to rewrite this code to change the error handling now. 436 $DB->force_transaction_rollback(); 437 return false; 438 } 439 440 $transaction->allow_commit(); 441 442 if (!empty($result->notice)) { 443 echo $OUTPUT->notification($result->notice); 444 return true; 445 } 446 447 // Give the question a unique version stamp determined by question_hash() 448 $DB->set_field('question', 'version', question_hash($question), 449 array('id' => $question->id)); 450 } 451 return true; 452 } 453 454 /** 455 * Count all non-category questions in the questions array. 456 * 457 * @param array questions An array of question objects. 458 * @return int The count. 459 * 460 */ 461 protected function count_questions($questions) { 462 $count = 0; 463 if (!is_array($questions)) { 464 return $count; 465 } 466 foreach ($questions as $question) { 467 if (!is_object($question) || !isset($question->qtype) || 468 ($question->qtype == 'category')) { 469 continue; 470 } 471 $count++; 472 } 473 return $count; 474 } 475 476 /** 477 * find and/or create the category described by a delimited list 478 * e.g. $course$/tom/dick/harry or tom/dick/harry 479 * 480 * removes any context string no matter whether $getcontext is set 481 * but if $getcontext is set then ignore the context and use selected category context. 482 * 483 * @param string catpath delimited category path 484 * @param int courseid course to search for categories 485 * @return mixed category object or null if fails 486 */ 487 protected function create_category_path($catpath) { 488 global $DB; 489 $catnames = $this->split_category_path($catpath); 490 $parent = 0; 491 $category = null; 492 493 // check for context id in path, it might not be there in pre 1.9 exports 494 $matchcount = preg_match('/^\$([a-z]+)\$$/', $catnames[0], $matches); 495 if ($matchcount == 1) { 496 $contextid = $this->translator->string_to_context($matches[1]); 497 array_shift($catnames); 498 } else { 499 $contextid = false; 500 } 501 502 if ($this->contextfromfile && $contextid !== false) { 503 $context = context::instance_by_id($contextid); 504 require_capability('moodle/question:add', $context); 505 } else { 506 $context = context::instance_by_id($this->category->contextid); 507 } 508 $this->importcontext = $context; 509 510 // Now create any categories that need to be created. 511 foreach ($catnames as $catname) { 512 if ($category = $DB->get_record('question_categories', 513 array('name' => $catname, 'contextid' => $context->id, 'parent' => $parent))) { 514 $parent = $category->id; 515 } else { 516 require_capability('moodle/question:managecategory', $context); 517 // create the new category 518 $category = new stdClass(); 519 $category->contextid = $context->id; 520 $category->name = $catname; 521 $category->info = ''; 522 $category->parent = $parent; 523 $category->sortorder = 999; 524 $category->stamp = make_unique_id_code(); 525 $id = $DB->insert_record('question_categories', $category); 526 $category->id = $id; 527 $parent = $id; 528 } 529 } 530 return $category; 531 } 532 533 /** 534 * Return complete file within an array, one item per line 535 * @param string filename name of file 536 * @return mixed contents array or false on failure 537 */ 538 protected function readdata($filename) { 539 if (is_readable($filename)) { 540 $filearray = file($filename); 541 542 // If the first line of the file starts with a UTF-8 BOM, remove it. 543 $filearray[0] = core_text::trim_utf8_bom($filearray[0]); 544 545 // Check for Macintosh OS line returns (ie file on one line), and fix. 546 if (preg_match("~\r~", $filearray[0]) AND !preg_match("~\n~", $filearray[0])) { 547 return explode("\r", $filearray[0]); 548 } else { 549 return $filearray; 550 } 551 } 552 return false; 553 } 554 555 /** 556 * Parses an array of lines into an array of questions, 557 * where each item is a question object as defined by 558 * readquestion(). Questions are defined as anything 559 * between blank lines. 560 * 561 * NOTE this method used to take $context as a second argument. However, at 562 * the point where this method was called, it was impossible to know what 563 * context the quetsions were going to be saved into, so the value could be 564 * wrong. Also, none of the standard question formats were using this argument, 565 * so it was removed. See MDL-32220. 566 * 567 * If your format does not use blank lines as a delimiter 568 * then you will need to override this method. Even then 569 * try to use readquestion for each question 570 * @param array lines array of lines from readdata 571 * @return array array of question objects 572 */ 573 protected function readquestions($lines) { 574 575 $questions = array(); 576 $currentquestion = array(); 577 578 foreach ($lines as $line) { 579 $line = trim($line); 580 if (empty($line)) { 581 if (!empty($currentquestion)) { 582 if ($question = $this->readquestion($currentquestion)) { 583 $questions[] = $question; 584 } 585 $currentquestion = array(); 586 } 587 } else { 588 $currentquestion[] = $line; 589 } 590 } 591 592 if (!empty($currentquestion)) { // There may be a final question 593 if ($question = $this->readquestion($currentquestion)) { 594 $questions[] = $question; 595 } 596 } 597 598 return $questions; 599 } 600 601 /** 602 * return an "empty" question 603 * Somewhere to specify question parameters that are not handled 604 * by import but are required db fields. 605 * This should not be overridden. 606 * @return object default question 607 */ 608 protected function defaultquestion() { 609 global $CFG; 610 static $defaultshuffleanswers = null; 611 if (is_null($defaultshuffleanswers)) { 612 $defaultshuffleanswers = get_config('quiz', 'shuffleanswers'); 613 } 614 615 $question = new stdClass(); 616 $question->shuffleanswers = $defaultshuffleanswers; 617 $question->defaultmark = 1; 618 $question->image = ""; 619 $question->usecase = 0; 620 $question->multiplier = array(); 621 $question->questiontextformat = FORMAT_MOODLE; 622 $question->generalfeedback = ''; 623 $question->generalfeedbackformat = FORMAT_MOODLE; 624 $question->correctfeedback = ''; 625 $question->partiallycorrectfeedback = ''; 626 $question->incorrectfeedback = ''; 627 $question->answernumbering = 'abc'; 628 $question->penalty = 0.3333333; 629 $question->length = 1; 630 631 // this option in case the questiontypes class wants 632 // to know where the data came from 633 $question->export_process = true; 634 $question->import_process = true; 635 636 return $question; 637 } 638 639 /** 640 * Construct a reasonable default question name, based on the start of the question text. 641 * @param string $questiontext the question text. 642 * @param string $default default question name to use if the constructed one comes out blank. 643 * @return string a reasonable question name. 644 */ 645 public function create_default_question_name($questiontext, $default) { 646 $name = $this->clean_question_name(shorten_text($questiontext, 80)); 647 if ($name) { 648 return $name; 649 } else { 650 return $default; 651 } 652 } 653 654 /** 655 * Ensure that a question name does not contain anything nasty, and will fit in the DB field. 656 * @param string $name the raw question name. 657 * @return string a safe question name. 658 */ 659 public function clean_question_name($name) { 660 $name = clean_param($name, PARAM_TEXT); // Matches what the question editing form does. 661 $name = trim($name); 662 $trimlength = 251; 663 while (core_text::strlen($name) > 255 && $trimlength > 0) { 664 $name = shorten_text($name, $trimlength); 665 $trimlength -= 10; 666 } 667 return $name; 668 } 669 670 /** 671 * Add a blank combined feedback to a question object. 672 * @param object question 673 * @return object question 674 */ 675 protected function add_blank_combined_feedback($question) { 676 $question->correctfeedback['text'] = ''; 677 $question->correctfeedback['format'] = $question->questiontextformat; 678 $question->correctfeedback['files'] = array(); 679 $question->partiallycorrectfeedback['text'] = ''; 680 $question->partiallycorrectfeedback['format'] = $question->questiontextformat; 681 $question->partiallycorrectfeedback['files'] = array(); 682 $question->incorrectfeedback['text'] = ''; 683 $question->incorrectfeedback['format'] = $question->questiontextformat; 684 $question->incorrectfeedback['files'] = array(); 685 return $question; 686 } 687 688 /** 689 * Given the data known to define a question in 690 * this format, this function converts it into a question 691 * object suitable for processing and insertion into Moodle. 692 * 693 * If your format does not use blank lines to delimit questions 694 * (e.g. an XML format) you must override 'readquestions' too 695 * @param $lines mixed data that represents question 696 * @return object question object 697 */ 698 protected function readquestion($lines) { 699 // We should never get there unless the qformat plugin is broken. 700 throw new coding_exception('Question format plugin is missing important code: readquestion.'); 701 702 return null; 703 } 704 705 /** 706 * Override if any post-processing is required 707 * @return bool success 708 */ 709 public function importpostprocess() { 710 return true; 711 } 712 713 /******************* 714 * EXPORT FUNCTIONS 715 *******************/ 716 717 /** 718 * Provide export functionality for plugin questiontypes 719 * Do not override 720 * @param name questiontype name 721 * @param question object data to export 722 * @param extra mixed any addition format specific data needed 723 * @return string the data to append to export or false if error (or unhandled) 724 */ 725 protected function try_exporting_using_qtypes($name, $question, $extra=null) { 726 // work out the name of format in use 727 $formatname = substr(get_class($this), strlen('qformat_')); 728 $methodname = "export_to_{$formatname}"; 729 730 $qtype = question_bank::get_qtype($name, false); 731 if (method_exists($qtype, $methodname)) { 732 return $qtype->$methodname($question, $this, $extra); 733 } 734 return false; 735 } 736 737 /** 738 * Do any pre-processing that may be required 739 * @param bool success 740 */ 741 public function exportpreprocess() { 742 return true; 743 } 744 745 /** 746 * Enable any processing to be done on the content 747 * just prior to the file being saved 748 * default is to do nothing 749 * @param string output text 750 * @param string processed output text 751 */ 752 protected function presave_process($content) { 753 return $content; 754 } 755 756 /** 757 * Do the export 758 * For most types this should not need to be overrided 759 * @return stored_file 760 */ 761 public function exportprocess() { 762 global $CFG, $OUTPUT, $DB, $USER; 763 764 // get the questions (from database) in this category 765 // only get q's with no parents (no cloze subquestions specifically) 766 if ($this->category) { 767 $questions = get_questions_category($this->category, true); 768 } else { 769 $questions = $this->questions; 770 } 771 772 $count = 0; 773 774 // results are first written into string (and then to a file) 775 // so create/initialize the string here 776 $expout = ""; 777 778 // track which category questions are in 779 // if it changes we will record the category change in the output 780 // file if selected. 0 means that it will get printed before the 1st question 781 $trackcategory = 0; 782 783 // iterate through questions 784 foreach ($questions as $question) { 785 // used by file api 786 $contextid = $DB->get_field('question_categories', 'contextid', 787 array('id' => $question->category)); 788 $question->contextid = $contextid; 789 790 // do not export hidden questions 791 if (!empty($question->hidden)) { 792 continue; 793 } 794 795 // do not export random questions 796 if ($question->qtype == 'random') { 797 continue; 798 } 799 800 // check if we need to record category change 801 if ($this->cattofile) { 802 if ($question->category != $trackcategory) { 803 $trackcategory = $question->category; 804 $categoryname = $this->get_category_path($trackcategory, $this->contexttofile); 805 806 // create 'dummy' question for category export 807 $dummyquestion = new stdClass(); 808 $dummyquestion->qtype = 'category'; 809 $dummyquestion->category = $categoryname; 810 $dummyquestion->name = 'Switch category to ' . $categoryname; 811 $dummyquestion->id = 0; 812 $dummyquestion->questiontextformat = ''; 813 $dummyquestion->contextid = 0; 814 $expout .= $this->writequestion($dummyquestion) . "\n"; 815 } 816 } 817 818 // export the question displaying message 819 $count++; 820 821 if (question_has_capability_on($question, 'view', $question->category)) { 822 $expout .= $this->writequestion($question, $contextid) . "\n"; 823 } 824 } 825 826 // continue path for following error checks 827 $course = $this->course; 828 $continuepath = "{$CFG->wwwroot}/question/export.php?courseid={$course->id}"; 829 830 // did we actually process anything 831 if ($count==0) { 832 print_error('noquestions', 'question', $continuepath); 833 } 834 835 // final pre-process on exported data 836 $expout = $this->presave_process($expout); 837 return $expout; 838 } 839 840 /** 841 * get the category as a path (e.g., tom/dick/harry) 842 * @param int id the id of the most nested catgory 843 * @return string the path 844 */ 845 protected function get_category_path($id, $includecontext = true) { 846 global $DB; 847 848 if (!$category = $DB->get_record('question_categories', array('id' => $id))) { 849 print_error('cannotfindcategory', 'error', '', $id); 850 } 851 $contextstring = $this->translator->context_to_string($category->contextid); 852 853 $pathsections = array(); 854 do { 855 $pathsections[] = $category->name; 856 $id = $category->parent; 857 } while ($category = $DB->get_record('question_categories', array('id' => $id))); 858 859 if ($includecontext) { 860 $pathsections[] = '$' . $contextstring . '$'; 861 } 862 863 $path = $this->assemble_category_path(array_reverse($pathsections)); 864 865 return $path; 866 } 867 868 /** 869 * Convert a list of category names, possibly preceeded by one of the 870 * context tokens like $course$, into a string representation of the 871 * category path. 872 * 873 * Names are separated by / delimiters. And /s in the name are replaced by //. 874 * 875 * To reverse the process and split the paths into names, use 876 * {@link split_category_path()}. 877 * 878 * @param array $names 879 * @return string 880 */ 881 protected function assemble_category_path($names) { 882 $escapednames = array(); 883 foreach ($names as $name) { 884 $escapedname = str_replace('/', '//', $name); 885 if (substr($escapedname, 0, 1) == '/') { 886 $escapedname = ' ' . $escapedname; 887 } 888 if (substr($escapedname, -1) == '/') { 889 $escapedname = $escapedname . ' '; 890 } 891 $escapednames[] = $escapedname; 892 } 893 return implode('/', $escapednames); 894 } 895 896 /** 897 * Convert a string, as returned by {@link assemble_category_path()}, 898 * back into an array of category names. 899 * 900 * Each category name is cleaned by a call to clean_param(, PARAM_TEXT), 901 * which matches the cleaning in question/category_form.php. 902 * 903 * @param string $path 904 * @return array of category names. 905 */ 906 protected function split_category_path($path) { 907 $rawnames = preg_split('~(?<!/)/(?!/)~', $path); 908 $names = array(); 909 foreach ($rawnames as $rawname) { 910 $names[] = clean_param(trim(str_replace('//', '/', $rawname)), PARAM_TEXT); 911 } 912 return $names; 913 } 914 915 /** 916 * Do an post-processing that may be required 917 * @return bool success 918 */ 919 protected function exportpostprocess() { 920 return true; 921 } 922 923 /** 924 * convert a single question object into text output in the given 925 * format. 926 * This must be overriden 927 * @param object question question object 928 * @return mixed question export text or null if not implemented 929 */ 930 protected function writequestion($question) { 931 // if not overidden, then this is an error. 932 throw new coding_exception('Question format plugin is missing important code: writequestion.'); 933 return null; 934 } 935 936 /** 937 * Convert the question text to plain text, so it can safely be displayed 938 * during import to let the user see roughly what is going on. 939 */ 940 protected function format_question_text($question) { 941 return question_utils::to_plain_text($question->questiontext, 942 $question->questiontextformat); 943 } 944 } 945 946 class qformat_based_on_xml extends qformat_default { 947 948 /** 949 * A lot of imported files contain unwanted entities. 950 * This method tries to clean up all known problems. 951 * @param string str string to correct 952 * @return string the corrected string 953 */ 954 public function cleaninput($str) { 955 956 $html_code_list = array( 957 "'" => "'", 958 "’" => "'", 959 "“" => "\"", 960 "”" => "\"", 961 "–" => "-", 962 "—" => "-", 963 ); 964 $str = strtr($str, $html_code_list); 965 // Use core_text entities_to_utf8 function to convert only numerical entities. 966 $str = core_text::entities_to_utf8($str, false); 967 return $str; 968 } 969 970 /** 971 * Return the array moodle is expecting 972 * for an HTML text. No processing is done on $text. 973 * qformat classes that want to process $text 974 * for instance to import external images files 975 * and recode urls in $text must overwrite this method. 976 * @param array $text some HTML text string 977 * @return array with keys text, format and files. 978 */ 979 public function text_field($text) { 980 return array( 981 'text' => trim($text), 982 'format' => FORMAT_HTML, 983 'files' => array(), 984 ); 985 } 986 987 /** 988 * Return the value of a node, given a path to the node 989 * if it doesn't exist return the default value. 990 * @param array xml data to read 991 * @param array path path to node expressed as array 992 * @param mixed default 993 * @param bool istext process as text 994 * @param string error if set value must exist, return false and issue message if not 995 * @return mixed value 996 */ 997 public function getpath($xml, $path, $default, $istext=false, $error='') { 998 foreach ($path as $index) { 999 if (!isset($xml[$index])) { 1000 if (!empty($error)) { 1001 $this->error($error); 1002 return false; 1003 } else { 1004 return $default; 1005 } 1006 } 1007 1008 $xml = $xml[$index]; 1009 } 1010 1011 if ($istext) { 1012 if (!is_string($xml)) { 1013 $this->error(get_string('invalidxml', 'qformat_xml')); 1014 } 1015 $xml = trim($xml); 1016 } 1017 1018 return $xml; 1019 } 1020 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |