[ 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 * 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 // reset the timer in case file upload was slow 293 core_php_time_limit::raise(); 294 295 // STAGE 1: Parse the file 296 echo $OUTPUT->notification(get_string('parsingquestions', 'question'), 'notifysuccess'); 297 298 if (! $lines = $this->readdata($this->filename)) { 299 echo $OUTPUT->notification(get_string('cannotread', 'question')); 300 return false; 301 } 302 303 if (!$questions = $this->readquestions($lines)) { // Extract all the questions 304 echo $OUTPUT->notification(get_string('noquestionsinfile', 'question')); 305 return false; 306 } 307 308 // STAGE 2: Write data to database 309 echo $OUTPUT->notification(get_string('importingquestions', 'question', 310 $this->count_questions($questions)), 'notifysuccess'); 311 312 // check for errors before we continue 313 if ($this->stoponerror and ($this->importerrors>0)) { 314 echo $OUTPUT->notification(get_string('importparseerror', 'question')); 315 return true; 316 } 317 318 // get list of valid answer grades 319 $gradeoptionsfull = question_bank::fraction_options_full(); 320 321 // check answer grades are valid 322 // (now need to do this here because of 'stop on error': MDL-10689) 323 $gradeerrors = 0; 324 $goodquestions = array(); 325 foreach ($questions as $question) { 326 if (!empty($question->fraction) and (is_array($question->fraction))) { 327 $fractions = $question->fraction; 328 $invalidfractions = array(); 329 foreach ($fractions as $key => $fraction) { 330 $newfraction = match_grade_options($gradeoptionsfull, $fraction, 331 $this->matchgrades); 332 if ($newfraction === false) { 333 $invalidfractions[] = $fraction; 334 } else { 335 $fractions[$key] = $newfraction; 336 } 337 } 338 if ($invalidfractions) { 339 echo $OUTPUT->notification(get_string('invalidgrade', 'question', 340 implode(', ', $invalidfractions))); 341 ++$gradeerrors; 342 continue; 343 } else { 344 $question->fraction = $fractions; 345 } 346 } 347 $goodquestions[] = $question; 348 } 349 $questions = $goodquestions; 350 351 // check for errors before we continue 352 if ($this->stoponerror && $gradeerrors > 0) { 353 return false; 354 } 355 356 // count number of questions processed 357 $count = 0; 358 359 foreach ($questions as $question) { // Process and store each question 360 $transaction = $DB->start_delegated_transaction(); 361 362 // reset the php timeout 363 core_php_time_limit::raise(); 364 365 // check for category modifiers 366 if ($question->qtype == 'category') { 367 if ($this->catfromfile) { 368 // find/create category object 369 $catpath = $question->category; 370 $newcategory = $this->create_category_path($catpath); 371 if (!empty($newcategory)) { 372 $this->category = $newcategory; 373 } 374 } 375 $transaction->allow_commit(); 376 continue; 377 } 378 $question->context = $this->importcontext; 379 380 $count++; 381 382 echo "<hr /><p><b>{$count}</b>. ".$this->format_question_text($question)."</p>"; 383 384 $question->category = $this->category->id; 385 $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed) 386 387 $question->createdby = $USER->id; 388 $question->timecreated = time(); 389 $question->modifiedby = $USER->id; 390 $question->timemodified = time(); 391 $fileoptions = array( 392 'subdirs' => true, 393 'maxfiles' => -1, 394 'maxbytes' => 0, 395 ); 396 397 $question->id = $DB->insert_record('question', $question); 398 399 if (isset($question->questiontextitemid)) { 400 $question->questiontext = file_save_draft_area_files($question->questiontextitemid, 401 $this->importcontext->id, 'question', 'questiontext', $question->id, 402 $fileoptions, $question->questiontext); 403 } else if (isset($question->questiontextfiles)) { 404 foreach ($question->questiontextfiles as $file) { 405 question_bank::get_qtype($question->qtype)->import_file( 406 $this->importcontext, 'question', 'questiontext', $question->id, $file); 407 } 408 } 409 if (isset($question->generalfeedbackitemid)) { 410 $question->generalfeedback = file_save_draft_area_files($question->generalfeedbackitemid, 411 $this->importcontext->id, 'question', 'generalfeedback', $question->id, 412 $fileoptions, $question->generalfeedback); 413 } else if (isset($question->generalfeedbackfiles)) { 414 foreach ($question->generalfeedbackfiles as $file) { 415 question_bank::get_qtype($question->qtype)->import_file( 416 $this->importcontext, 'question', 'generalfeedback', $question->id, $file); 417 } 418 } 419 $DB->update_record('question', $question); 420 421 $this->questionids[] = $question->id; 422 423 // Now to save all the answers and type-specific options 424 425 $result = question_bank::get_qtype($question->qtype)->save_question_options($question); 426 427 if (!empty($CFG->usetags) && isset($question->tags)) { 428 require_once($CFG->dirroot . '/tag/lib.php'); 429 tag_set('question', $question->id, $question->tags, 'core_question', $question->context->id); 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 700 $formatnotimplemented = get_string('formatnotimplemented', 'question'); 701 echo "<p>{$formatnotimplemented}</p>"; 702 703 return null; 704 } 705 706 /** 707 * Override if any post-processing is required 708 * @return bool success 709 */ 710 public function importpostprocess() { 711 return true; 712 } 713 714 /******************* 715 * EXPORT FUNCTIONS 716 *******************/ 717 718 /** 719 * Provide export functionality for plugin questiontypes 720 * Do not override 721 * @param name questiontype name 722 * @param question object data to export 723 * @param extra mixed any addition format specific data needed 724 * @return string the data to append to export or false if error (or unhandled) 725 */ 726 protected function try_exporting_using_qtypes($name, $question, $extra=null) { 727 // work out the name of format in use 728 $formatname = substr(get_class($this), strlen('qformat_')); 729 $methodname = "export_to_{$formatname}"; 730 731 $qtype = question_bank::get_qtype($name, false); 732 if (method_exists($qtype, $methodname)) { 733 return $qtype->$methodname($question, $this, $extra); 734 } 735 return false; 736 } 737 738 /** 739 * Do any pre-processing that may be required 740 * @param bool success 741 */ 742 public function exportpreprocess() { 743 return true; 744 } 745 746 /** 747 * Enable any processing to be done on the content 748 * just prior to the file being saved 749 * default is to do nothing 750 * @param string output text 751 * @param string processed output text 752 */ 753 protected function presave_process($content) { 754 return $content; 755 } 756 757 /** 758 * Do the export 759 * For most types this should not need to be overrided 760 * @return stored_file 761 */ 762 public function exportprocess() { 763 global $CFG, $OUTPUT, $DB, $USER; 764 765 // get the questions (from database) in this category 766 // only get q's with no parents (no cloze subquestions specifically) 767 if ($this->category) { 768 $questions = get_questions_category($this->category, true); 769 } else { 770 $questions = $this->questions; 771 } 772 773 $count = 0; 774 775 // results are first written into string (and then to a file) 776 // so create/initialize the string here 777 $expout = ""; 778 779 // track which category questions are in 780 // if it changes we will record the category change in the output 781 // file if selected. 0 means that it will get printed before the 1st question 782 $trackcategory = 0; 783 784 // iterate through questions 785 foreach ($questions as $question) { 786 // used by file api 787 $contextid = $DB->get_field('question_categories', 'contextid', 788 array('id' => $question->category)); 789 $question->contextid = $contextid; 790 791 // do not export hidden questions 792 if (!empty($question->hidden)) { 793 continue; 794 } 795 796 // do not export random questions 797 if ($question->qtype == 'random') { 798 continue; 799 } 800 801 // check if we need to record category change 802 if ($this->cattofile) { 803 if ($question->category != $trackcategory) { 804 $trackcategory = $question->category; 805 $categoryname = $this->get_category_path($trackcategory, $this->contexttofile); 806 807 // create 'dummy' question for category export 808 $dummyquestion = new stdClass(); 809 $dummyquestion->qtype = 'category'; 810 $dummyquestion->category = $categoryname; 811 $dummyquestion->name = 'Switch category to ' . $categoryname; 812 $dummyquestion->id = 0; 813 $dummyquestion->questiontextformat = ''; 814 $dummyquestion->contextid = 0; 815 $expout .= $this->writequestion($dummyquestion) . "\n"; 816 } 817 } 818 819 // export the question displaying message 820 $count++; 821 822 if (question_has_capability_on($question, 'view', $question->category)) { 823 $expout .= $this->writequestion($question, $contextid) . "\n"; 824 } 825 } 826 827 // continue path for following error checks 828 $course = $this->course; 829 $continuepath = "{$CFG->wwwroot}/question/export.php?courseid={$course->id}"; 830 831 // did we actually process anything 832 if ($count==0) { 833 print_error('noquestions', 'question', $continuepath); 834 } 835 836 // final pre-process on exported data 837 $expout = $this->presave_process($expout); 838 return $expout; 839 } 840 841 /** 842 * get the category as a path (e.g., tom/dick/harry) 843 * @param int id the id of the most nested catgory 844 * @return string the path 845 */ 846 protected function get_category_path($id, $includecontext = true) { 847 global $DB; 848 849 if (!$category = $DB->get_record('question_categories', array('id' => $id))) { 850 print_error('cannotfindcategory', 'error', '', $id); 851 } 852 $contextstring = $this->translator->context_to_string($category->contextid); 853 854 $pathsections = array(); 855 do { 856 $pathsections[] = $category->name; 857 $id = $category->parent; 858 } while ($category = $DB->get_record('question_categories', array('id' => $id))); 859 860 if ($includecontext) { 861 $pathsections[] = '$' . $contextstring . '$'; 862 } 863 864 $path = $this->assemble_category_path(array_reverse($pathsections)); 865 866 return $path; 867 } 868 869 /** 870 * Convert a list of category names, possibly preceeded by one of the 871 * context tokens like $course$, into a string representation of the 872 * category path. 873 * 874 * Names are separated by / delimiters. And /s in the name are replaced by //. 875 * 876 * To reverse the process and split the paths into names, use 877 * {@link split_category_path()}. 878 * 879 * @param array $names 880 * @return string 881 */ 882 protected function assemble_category_path($names) { 883 $escapednames = array(); 884 foreach ($names as $name) { 885 $escapedname = str_replace('/', '//', $name); 886 if (substr($escapedname, 0, 1) == '/') { 887 $escapedname = ' ' . $escapedname; 888 } 889 if (substr($escapedname, -1) == '/') { 890 $escapedname = $escapedname . ' '; 891 } 892 $escapednames[] = $escapedname; 893 } 894 return implode('/', $escapednames); 895 } 896 897 /** 898 * Convert a string, as returned by {@link assemble_category_path()}, 899 * back into an array of category names. 900 * 901 * Each category name is cleaned by a call to clean_param(, PARAM_TEXT), 902 * which matches the cleaning in question/category_form.php. 903 * 904 * @param string $path 905 * @return array of category names. 906 */ 907 protected function split_category_path($path) { 908 $rawnames = preg_split('~(?<!/)/(?!/)~', $path); 909 $names = array(); 910 foreach ($rawnames as $rawname) { 911 $names[] = clean_param(trim(str_replace('//', '/', $rawname)), PARAM_TEXT); 912 } 913 return $names; 914 } 915 916 /** 917 * Do an post-processing that may be required 918 * @return bool success 919 */ 920 protected function exportpostprocess() { 921 return true; 922 } 923 924 /** 925 * convert a single question object into text output in the given 926 * format. 927 * This must be overriden 928 * @param object question question object 929 * @return mixed question export text or null if not implemented 930 */ 931 protected function writequestion($question) { 932 // if not overidden, then this is an error. 933 $formatnotimplemented = get_string('formatnotimplemented', 'question'); 934 echo "<p>{$formatnotimplemented}</p>"; 935 return null; 936 } 937 938 /** 939 * Convert the question text to plain text, so it can safely be displayed 940 * during import to let the user see roughly what is going on. 941 */ 942 protected function format_question_text($question) { 943 return question_utils::to_plain_text($question->questiontext, 944 $question->questiontextformat); 945 } 946 } 947 948 class qformat_based_on_xml extends qformat_default { 949 950 /** 951 * A lot of imported files contain unwanted entities. 952 * This method tries to clean up all known problems. 953 * @param string str string to correct 954 * @return string the corrected string 955 */ 956 public function cleaninput($str) { 957 958 $html_code_list = array( 959 "'" => "'", 960 "’" => "'", 961 "“" => "\"", 962 "”" => "\"", 963 "–" => "-", 964 "—" => "-", 965 ); 966 $str = strtr($str, $html_code_list); 967 // Use core_text entities_to_utf8 function to convert only numerical entities. 968 $str = core_text::entities_to_utf8($str, false); 969 return $str; 970 } 971 972 /** 973 * Return the array moodle is expecting 974 * for an HTML text. No processing is done on $text. 975 * qformat classes that want to process $text 976 * for instance to import external images files 977 * and recode urls in $text must overwrite this method. 978 * @param array $text some HTML text string 979 * @return array with keys text, format and files. 980 */ 981 public function text_field($text) { 982 return array( 983 'text' => trim($text), 984 'format' => FORMAT_HTML, 985 'files' => array(), 986 ); 987 } 988 989 /** 990 * Return the value of a node, given a path to the node 991 * if it doesn't exist return the default value. 992 * @param array xml data to read 993 * @param array path path to node expressed as array 994 * @param mixed default 995 * @param bool istext process as text 996 * @param string error if set value must exist, return false and issue message if not 997 * @return mixed value 998 */ 999 public function getpath($xml, $path, $default, $istext=false, $error='') { 1000 foreach ($path as $index) { 1001 if (!isset($xml[$index])) { 1002 if (!empty($error)) { 1003 $this->error($error); 1004 return false; 1005 } else { 1006 return $default; 1007 } 1008 } 1009 1010 $xml = $xml[$index]; 1011 } 1012 1013 if ($istext) { 1014 if (!is_string($xml)) { 1015 $this->error(get_string('invalidxml', 'qformat_xml')); 1016 } 1017 $xml = trim($xml); 1018 } 1019 1020 return $xml; 1021 } 1022 }
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 |