| [ Index ] |
PHP Cross Reference of Moodle 1.9.3 [Build 15-Oct-2008] |
[Summary view] [Print] [Text view]
1 <?php // $Id: format.php,v 1.35.2.11 2008/08/28 13:43:25 thepurpleblob Exp $ 2 /** 3 * Base class for question import and export formats. 4 * 5 * @author Martin Dougiamas, Howard Miller, and many others. 6 * {@link http://moodle.org} 7 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License 8 * @package questionbank 9 * @subpackage importexport 10 */ 11 class qformat_default { 12 13 var $displayerrors = true; 14 var $category = NULL; 15 var $questions = array(); 16 var $course = NULL; 17 var $filename = ''; 18 var $realfilename = ''; 19 var $matchgrades = 'error'; 20 var $catfromfile = 0; 21 var $contextfromfile = 0; 22 var $cattofile = 0; 23 var $contexttofile = 0; 24 var $questionids = array(); 25 var $importerrors = 0; 26 var $stoponerror = true; 27 var $translator = null; 28 var $canaccessbackupdata = true; 29 30 31 // functions to indicate import/export functionality 32 // override to return true if implemented 33 34 function provide_import() { 35 return false; 36 } 37 38 function provide_export() { 39 return false; 40 } 41 42 // Accessor methods 43 44 /** 45 * set the category 46 * @param object category the category object 47 */ 48 function setCategory( $category ) { 49 if (count($this->questions)){ 50 debugging('You shouldn\'t call setCategory after setQuestions'); 51 } 52 $this->category = $category; 53 } 54 55 /** 56 * Set the specific questions to export. Should not include questions with 57 * parents (sub questions of cloze question type). 58 * Only used for question export. 59 * @param array of question objects 60 */ 61 function setQuestions( $questions ) { 62 if ($this->category !== null){ 63 debugging('You shouldn\'t call setQuestions after setCategory'); 64 } 65 $this->questions = $questions; 66 } 67 68 /** 69 * set the course class variable 70 * @param course object Moodle course variable 71 */ 72 function setCourse( $course ) { 73 $this->course = $course; 74 } 75 /** 76 * set an array of contexts. 77 * @param array $contexts Moodle course variable 78 */ 79 function setContexts($contexts) { 80 $this->contexts = $contexts; 81 $this->translator = new context_to_string_translator($this->contexts); 82 } 83 84 /** 85 * set the filename 86 * @param string filename name of file to import/export 87 */ 88 function setFilename( $filename ) { 89 $this->filename = $filename; 90 } 91 92 /** 93 * set the "real" filename 94 * (this is what the user typed, regardless of wha happened next) 95 * @param string realfilename name of file as typed by user 96 */ 97 function setRealfilename( $realfilename ) { 98 $this->realfilename = $realfilename; 99 } 100 101 /** 102 * set matchgrades 103 * @param string matchgrades error or nearest for grades 104 */ 105 function setMatchgrades( $matchgrades ) { 106 $this->matchgrades = $matchgrades; 107 } 108 109 /** 110 * set catfromfile 111 * @param bool catfromfile allow categories embedded in import file 112 */ 113 function setCatfromfile( $catfromfile ) { 114 $this->catfromfile = $catfromfile; 115 } 116 117 /** 118 * set contextfromfile 119 * @param bool $contextfromfile allow contexts embedded in import file 120 */ 121 function setContextfromfile($contextfromfile) { 122 $this->contextfromfile = $contextfromfile; 123 } 124 125 /** 126 * set cattofile 127 * @param bool cattofile exports categories within export file 128 */ 129 function setCattofile( $cattofile ) { 130 $this->cattofile = $cattofile; 131 } 132 /** 133 * set contexttofile 134 * @param bool cattofile exports categories within export file 135 */ 136 function setContexttofile($contexttofile) { 137 $this->contexttofile = $contexttofile; 138 } 139 140 /** 141 * set stoponerror 142 * @param bool stoponerror stops database write if any errors reported 143 */ 144 function setStoponerror( $stoponerror ) { 145 $this->stoponerror = $stoponerror; 146 } 147 148 /** 149 * @param boolean $canaccess Whether the current use can access the backup data folder. Determines 150 * where export files are saved. 151 */ 152 function set_can_access_backupdata($canaccess) { 153 $this->canaccessbackupdata = $canaccess; 154 } 155 156 /*********************** 157 * IMPORTING FUNCTIONS 158 ***********************/ 159 160 /** 161 * Handle parsing error 162 */ 163 function error( $message, $text='', $questionname='' ) { 164 $importerrorquestion = get_string('importerrorquestion','quiz'); 165 166 echo "<div class=\"importerror\">\n"; 167 echo "<strong>$importerrorquestion $questionname</strong>"; 168 if (!empty($text)) { 169 $text = s($text); 170 echo "<blockquote>$text</blockquote>\n"; 171 } 172 echo "<strong>$message</strong>\n"; 173 echo "</div>"; 174 175 $this->importerrors++; 176 } 177 178 /** 179 * Import for questiontype plugins 180 * Do not override. 181 * @param data mixed The segment of data containing the question 182 * @param question object processed (so far) by standard import code if appropriate 183 * @param extra mixed any additional format specific data that may be passed by the format 184 * @return object question object suitable for save_options() or false if cannot handle 185 */ 186 function try_importing_using_qtypes( $data, $question=null, $extra=null ) { 187 global $QTYPES; 188 189 // work out what format we are using 190 $formatname = substr( get_class( $this ), strlen('qformat_')); 191 $methodname = "import_from_$formatname"; 192 193 // loop through installed questiontypes checking for 194 // function to handle this question 195 foreach ($QTYPES as $qtype) { 196 if (method_exists( $qtype, $methodname)) { 197 if ($question = $qtype->$methodname( $data, $question, $this, $extra )) { 198 return $question; 199 } 200 } 201 } 202 return false; 203 } 204 205 /** 206 * Perform any required pre-processing 207 * @return boolean success 208 */ 209 function importpreprocess() { 210 return true; 211 } 212 213 /** 214 * Process the file 215 * This method should not normally be overidden 216 * @return boolean success 217 */ 218 function importprocess() { 219 global $USER; 220 221 // reset the timer in case file upload was slow 222 @set_time_limit(); 223 224 // STAGE 1: Parse the file 225 notify( get_string('parsingquestions','quiz') ); 226 227 if (! $lines = $this->readdata($this->filename)) { 228 notify( get_string('cannotread','quiz') ); 229 return false; 230 } 231 232 if (! $questions = $this->readquestions($lines)) { // Extract all the questions 233 notify( get_string('noquestionsinfile','quiz') ); 234 return false; 235 } 236 237 // STAGE 2: Write data to database 238 notify( get_string('importingquestions','quiz',$this->count_questions($questions)) ); 239 240 // check for errors before we continue 241 if ($this->stoponerror and ($this->importerrors>0)) { 242 return false; 243 } 244 245 // get list of valid answer grades 246 $grades = get_grade_options(); 247 $gradeoptionsfull = $grades->gradeoptionsfull; 248 249 // check answer grades are valid 250 // (now need to do this here because of 'stop on error': MDL-10689) 251 $gradeerrors = 0; 252 $goodquestions = array(); 253 foreach ($questions as $question) { 254 if (!empty($question->fraction) and (is_array($question->fraction))) { 255 $fractions = $question->fraction; 256 $answersvalid = true; // in case they are! 257 foreach ($fractions as $key => $fraction) { 258 $newfraction = match_grade_options($gradeoptionsfull, $fraction, $this->matchgrades); 259 if ($newfraction===false) { 260 $answersvalid = false; 261 } 262 else { 263 $fractions[$key] = $newfraction; 264 } 265 } 266 if (!$answersvalid) { 267 notify(get_string('matcherror', 'quiz')); 268 ++$gradeerrors; 269 continue; 270 } 271 else { 272 $question->fraction = $fractions; 273 } 274 } 275 $goodquestions[] = $question; 276 } 277 $questions = $goodquestions; 278 279 // check for errors before we continue 280 if ($this->stoponerror and ($gradeerrors>0)) { 281 return false; 282 } 283 284 // count number of questions processed 285 $count = 0; 286 287 foreach ($questions as $question) { // Process and store each question 288 289 // reset the php timeout 290 @set_time_limit(); 291 292 // check for category modifiers 293 if ($question->qtype=='category') { 294 if ($this->catfromfile) { 295 // find/create category object 296 $catpath = $question->category; 297 $newcategory = $this->create_category_path( $catpath, '/'); 298 if (!empty($newcategory)) { 299 $this->category = $newcategory; 300 } 301 } 302 continue; 303 } 304 305 $count++; 306 307 echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>"; 308 309 $question->category = $this->category->id; 310 $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed) 311 312 $question->createdby = $USER->id; 313 $question->timecreated = time(); 314 315 if (!$question->id = insert_record("question", $question)) { 316 error( get_string('cannotinsert','quiz') ); 317 } 318 319 $this->questionids[] = $question->id; 320 321 // Now to save all the answers and type-specific options 322 323 global $QTYPES; 324 $result = $QTYPES[$question->qtype] 325 ->save_question_options($question); 326 327 if (!empty($result->error)) { 328 notify($result->error); 329 return false; 330 } 331 332 if (!empty($result->notice)) { 333 notify($result->notice); 334 return true; 335 } 336 337 // Give the question a unique version stamp determined by question_hash() 338 set_field('question', 'version', question_hash($question), 'id', $question->id); 339 } 340 return true; 341 } 342 /** 343 * Count all non-category questions in the questions array. 344 * 345 * @param array questions An array of question objects. 346 * @return int The count. 347 * 348 */ 349 function count_questions($questions) { 350 $count = 0; 351 if (!is_array($questions)) { 352 return $count; 353 } 354 foreach ($questions as $question) { 355 if (!is_object($question) || !isset($question->qtype) || ($question->qtype == 'category')) { 356 continue; 357 } 358 $count++; 359 } 360 return $count; 361 } 362 363 /** 364 * find and/or create the category described by a delimited list 365 * e.g. $course$/tom/dick/harry or tom/dick/harry 366 * 367 * removes any context string no matter whether $getcontext is set 368 * but if $getcontext is set then ignore the context and use selected category context. 369 * 370 * @param string catpath delimited category path 371 * @param string delimiter path delimiting character 372 * @param int courseid course to search for categories 373 * @return mixed category object or null if fails 374 */ 375 function create_category_path($catpath, $delimiter='/') { 376 $catpath = clean_param($catpath, PARAM_PATH); 377 $catnames = explode($delimiter, $catpath); 378 $parent = 0; 379 $category = null; 380 381 // check for context id in path, it might not be there in pre 1.9 exports 382 $matchcount = preg_match('/^\$([a-z]+)\$$/', $catnames[0], $matches); 383 if ($matchcount==1) { 384 $contextid = $this->translator->string_to_context($matches[1]); 385 array_shift($catnames); 386 } else { 387 $contextid = FALSE; 388 } 389 if ($this->contextfromfile && ($contextid !== FALSE)){ 390 $context = get_context_instance_by_id($contextid); 391 require_capability('moodle/question:add', $context); 392 } else { 393 $context = get_context_instance_by_id($this->category->contextid); 394 } 395 foreach ($catnames as $catname) { 396 if ($category = get_record( 'question_categories', 'name', $catname, 'contextid', $context->id, 'parent', $parent)) { 397 $parent = $category->id; 398 } else { 399 require_capability('moodle/question:managecategory', $context); 400 // create the new category 401 $category = new object; 402 $category->contextid = $context->id; 403 $category->name = $catname; 404 $category->info = ''; 405 $category->parent = $parent; 406 $category->sortorder = 999; 407 $category->stamp = make_unique_id_code(); 408 if (!($id = insert_record('question_categories', $category))) { 409 error( "cannot create new category - $catname" ); 410 } 411 $category->id = $id; 412 $parent = $id; 413 } 414 } 415 return $category; 416 } 417 418 /** 419 * Return complete file within an array, one item per line 420 * @param string filename name of file 421 * @return mixed contents array or false on failure 422 */ 423 function readdata($filename) { 424 if (is_readable($filename)) { 425 $filearray = file($filename); 426 427 /// Check for Macintosh OS line returns (ie file on one line), and fix 428 if (ereg("\r", $filearray[0]) AND !ereg("\n", $filearray[0])) { 429 return explode("\r", $filearray[0]); 430 } else { 431 return $filearray; 432 } 433 } 434 return false; 435 } 436 437 /** 438 * Parses an array of lines into an array of questions, 439 * where each item is a question object as defined by 440 * readquestion(). Questions are defined as anything 441 * between blank lines. 442 * 443 * If your format does not use blank lines as a delimiter 444 * then you will need to override this method. Even then 445 * try to use readquestion for each question 446 * @param array lines array of lines from readdata 447 * @return array array of question objects 448 */ 449 function readquestions($lines) { 450 451 $questions = array(); 452 $currentquestion = array(); 453 454 foreach ($lines as $line) { 455 $line = trim($line); 456 if (empty($line)) { 457 if (!empty($currentquestion)) { 458 if ($question = $this->readquestion($currentquestion)) { 459 $questions[] = $question; 460 } 461 $currentquestion = array(); 462 } 463 } else { 464 $currentquestion[] = $line; 465 } 466 } 467 468 if (!empty($currentquestion)) { // There may be a final question 469 if ($question = $this->readquestion($currentquestion)) { 470 $questions[] = $question; 471 } 472 } 473 474 return $questions; 475 } 476 477 478 /** 479 * return an "empty" question 480 * Somewhere to specify question parameters that are not handled 481 * by import but are required db fields. 482 * This should not be overridden. 483 * @return object default question 484 */ 485 function defaultquestion() { 486 global $CFG; 487 488 $question = new stdClass(); 489 $question->shuffleanswers = $CFG->quiz_shuffleanswers; 490 $question->defaultgrade = 1; 491 $question->image = ""; 492 $question->usecase = 0; 493 $question->multiplier = array(); 494 $question->generalfeedback = ''; 495 $question->correctfeedback = ''; 496 $question->partiallycorrectfeedback = ''; 497 $question->incorrectfeedback = ''; 498 $question->answernumbering = 'abc'; 499 $question->penalty = 0.1; 500 $question->length = 1; 501 502 // this option in case the questiontypes class wants 503 // to know where the data came from 504 $question->export_process = true; 505 $question->import_process = true; 506 507 return $question; 508 } 509 510 /** 511 * Given the data known to define a question in 512 * this format, this function converts it into a question 513 * object suitable for processing and insertion into Moodle. 514 * 515 * If your format does not use blank lines to delimit questions 516 * (e.g. an XML format) you must override 'readquestions' too 517 * @param $lines mixed data that represents question 518 * @return object question object 519 */ 520 function readquestion($lines) { 521 522 $formatnotimplemented = get_string( 'formatnotimplemented','quiz' ); 523 echo "<p>$formatnotimplemented</p>"; 524 525 return NULL; 526 } 527 528 /** 529 * Override if any post-processing is required 530 * @return boolean success 531 */ 532 function importpostprocess() { 533 return true; 534 } 535 536 /** 537 * Import an image file encoded in base64 format 538 * @param string path path (in course data) to store picture 539 * @param string base64 encoded picture 540 * @return string filename (nb. collisions are handled) 541 */ 542 function importimagefile( $path, $base64 ) { 543 global $CFG; 544 545 // all this to get the destination directory 546 // and filename! 547 $fullpath = "{$CFG->dataroot}/{$this->course->id}/$path"; 548 $path_parts = pathinfo( $fullpath ); 549 $destination = $path_parts['dirname']; 550 $file = clean_filename( $path_parts['basename'] ); 551 552 // check if path exists 553 check_dir_exists($destination, true, true ); 554 555 // detect and fix any filename collision - get unique filename 556 $newfiles = resolve_filename_collisions( $destination, array($file) ); 557 $newfile = $newfiles[0]; 558 559 // convert and save file contents 560 if (!$content = base64_decode( $base64 )) { 561 return ''; 562 } 563 $newfullpath = "$destination/$newfile"; 564 if (!$fh = fopen( $newfullpath, 'w' )) { 565 return ''; 566 } 567 if (!fwrite( $fh, $content )) { 568 return ''; 569 } 570 fclose( $fh ); 571 572 // return the (possibly) new filename 573 $newfile = ereg_replace("{$CFG->dataroot}/{$this->course->id}/", '',$newfullpath); 574 return $newfile; 575 } 576 577 578 /******************* 579 * EXPORT FUNCTIONS 580 *******************/ 581 582 /** 583 * Provide export functionality for plugin questiontypes 584 * Do not override 585 * @param name questiontype name 586 * @param question object data to export 587 * @param extra mixed any addition format specific data needed 588 * @return string the data to append to export or false if error (or unhandled) 589 */ 590 function try_exporting_using_qtypes( $name, $question, $extra=null ) { 591 global $QTYPES; 592 593 // work out the name of format in use 594 $formatname = substr( get_class( $this ), strlen( 'qformat_' )); 595 $methodname = "export_to_$formatname"; 596 597 if (array_key_exists( $name, $QTYPES )) { 598 $qtype = $QTYPES[ $name ]; 599 if (method_exists( $qtype, $methodname )) { 600 if ($data = $qtype->$methodname( $question, $this, $extra )) { 601 return $data; 602 } 603 } 604 } 605 return false; 606 } 607 608 /** 609 * Return the files extension appropriate for this type 610 * override if you don't want .txt 611 * @return string file extension 612 */ 613 function export_file_extension() { 614 return ".txt"; 615 } 616 617 /** 618 * Do any pre-processing that may be required 619 * @param boolean success 620 */ 621 function exportpreprocess() { 622 return true; 623 } 624 625 /** 626 * Enable any processing to be done on the content 627 * just prior to the file being saved 628 * default is to do nothing 629 * @param string output text 630 * @param string processed output text 631 */ 632 function presave_process( $content ) { 633 return $content; 634 } 635 636 /** 637 * Do the export 638 * For most types this should not need to be overrided 639 * @return boolean success 640 */ 641 function exportprocess() { 642 global $CFG; 643 644 // create a directory for the exports (if not already existing) 645 if (! $export_dir = make_upload_directory($this->question_get_export_dir())) { 646 error( get_string('cannotcreatepath','quiz',$export_dir) ); 647 } 648 $path = $CFG->dataroot.'/'.$this->question_get_export_dir(); 649 650 // get the questions (from database) in this category 651 // only get q's with no parents (no cloze subquestions specifically) 652 if ($this->category){ 653 $questions = get_questions_category( $this->category, true ); 654 } else { 655 $questions = $this->questions; 656 } 657 658 notify( get_string('exportingquestions','quiz') ); 659 $count = 0; 660 661 // results are first written into string (and then to a file) 662 // so create/initialize the string here 663 $expout = ""; 664 665 // track which category questions are in 666 // if it changes we will record the category change in the output 667 // file if selected. 0 means that it will get printed before the 1st question 668 $trackcategory = 0; 669 670 // iterate through questions 671 foreach($questions as $question) { 672 673 // do not export hidden questions 674 if (!empty($question->hidden)) { 675 continue; 676 } 677 678 // do not export random questions 679 if ($question->qtype==RANDOM) { 680 continue; 681 } 682 683 // check if we need to record category change 684 if ($this->cattofile) { 685 if ($question->category != $trackcategory) { 686 $trackcategory = $question->category; 687 $categoryname = $this->get_category_path($trackcategory, '/', $this->contexttofile); 688 689 // create 'dummy' question for category export 690 $dummyquestion = new object; 691 $dummyquestion->qtype = 'category'; 692 $dummyquestion->category = $categoryname; 693 $dummyquestion->name = "switch category to $categoryname"; 694 $dummyquestion->id = 0; 695 $dummyquestion->questiontextformat = ''; 696 $expout .= $this->writequestion( $dummyquestion ) . "\n"; 697 } 698 } 699 700 // export the question displaying message 701 $count++; 702 echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>"; 703 if (question_has_capability_on($question, 'view', $question->category)){ 704 $expout .= $this->writequestion( $question ) . "\n"; 705 } 706 } 707 708 // continue path for following error checks 709 $course = $this->course; 710 $continuepath = "$CFG->wwwroot/question/export.php?courseid=$course->id"; 711 712 // did we actually process anything 713 if ($count==0) { 714 print_error( 'noquestions','quiz',$continuepath ); 715 } 716 717 // final pre-process on exported data 718 $expout = $this->presave_process( $expout ); 719 720 // write file 721 $filepath = $path."/".$this->filename . $this->export_file_extension(); 722 if (!$fh=fopen($filepath,"w")) { 723 print_error( 'cannotopen','quiz',$continuepath,$filepath ); 724 } 725 if (!fwrite($fh, $expout, strlen($expout) )) { 726 print_error( 'cannotwrite','quiz',$continuepath,$filepath ); 727 } 728 fclose($fh); 729 return true; 730 } 731 732 /** 733 * get the category as a path (e.g., tom/dick/harry) 734 * @param int id the id of the most nested catgory 735 * @param string delimiter the delimiter you want 736 * @return string the path 737 */ 738 function get_category_path($id, $delimiter='/', $includecontext = true) { 739 $path = ''; 740 if (!$firstcategory = get_record('question_categories','id',$id)) { 741 error( "Error getting category record from db - " . $id ); 742 } 743 $category = $firstcategory; 744 $contextstring = $this->translator->context_to_string($category->contextid); 745 do { 746 $name = $category->name; 747 $id = $category->parent; 748 if (!empty($path)) { 749 $path = "{$name}{$delimiter}{$path}"; 750 } 751 else { 752 $path = $name; 753 } 754 } while ($category = get_record( 'question_categories','id',$id )); 755 756 if ($includecontext){ 757 $path = '$'.$contextstring.'$'."{$delimiter}{$path}"; 758 } 759 return $path; 760 } 761 762 /** 763 * Do an post-processing that may be required 764 * @return boolean success 765 */ 766 function exportpostprocess() { 767 return true; 768 } 769 770 /** 771 * convert a single question object into text output in the given 772 * format. 773 * This must be overriden 774 * @param object question question object 775 * @return mixed question export text or null if not implemented 776 */ 777 function writequestion($question) { 778 // if not overidden, then this is an error. 779 $formatnotimplemented = get_string( 'formatnotimplemented','quiz' ); 780 echo "<p>$formatnotimplemented</p>"; 781 return NULL; 782 } 783 784 /** 785 * get directory into which export is going 786 * @return string file path 787 */ 788 function question_get_export_dir() { 789 global $USER; 790 if ($this->canaccessbackupdata) { 791 $dirname = get_string("exportfilename","quiz"); 792 $path = $this->course->id.'/backupdata/'.$dirname; // backupdata is protected directory 793 } else { 794 $path = 'temp/questionexport/' . $USER->id; 795 } 796 return $path; 797 } 798 799 /** 800 * where question specifies a moodle (text) format this 801 * performs the conversion. 802 */ 803 function format_question_text($question) { 804 $formatoptions = new stdClass; 805 $formatoptions->noclean = true; 806 $formatoptions->para = false; 807 if (empty($question->questiontextformat)) { 808 $format = FORMAT_MOODLE; 809 } else { 810 $format = $question->questiontextformat; 811 } 812 return format_text(stripslashes($question->questiontext), $format, $formatoptions); 813 } 814 815 816 } 817 818 ?>
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Wed Jan 14 11:33:29 2009 | Cross-referenced by PHPXref 0.7 |