|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 // This file is part of Moodle - http://moodle.org/ 00003 // 00004 // Moodle is free software: you can redistribute it and/or modify 00005 // it under the terms of the GNU General Public License as published by 00006 // the Free Software Foundation, either version 3 of the License, or 00007 // (at your option) any later version. 00008 // 00009 // Moodle is distributed in the hope that it will be useful, 00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 // GNU General Public License for more details. 00013 // 00014 // You should have received a copy of the GNU General Public License 00015 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00016 00027 defined('MOODLE_INTERNAL') || die(); 00028 00029 00037 if (!defined('SHORTANSWER')) { 00038 define("SHORTANSWER", "shortanswer"); 00039 define("TRUEFALSE", "truefalse"); 00040 define("MULTICHOICE", "multichoice"); 00041 define("RANDOM", "random"); 00042 define("MATCH", "match"); 00043 define("RANDOMSAMATCH", "randomsamatch"); 00044 define("DESCRIPTION", "description"); 00045 define("NUMERICAL", "numerical"); 00046 define("MULTIANSWER", "multianswer"); 00047 define("CALCULATED", "calculated"); 00048 define("ESSAY", "essay"); 00049 } 00059 class qformat_default { 00060 00061 public $displayerrors = true; 00062 public $category = null; 00063 public $questions = array(); 00064 public $course = null; 00065 public $filename = ''; 00066 public $realfilename = ''; 00067 public $matchgrades = 'error'; 00068 public $catfromfile = 0; 00069 public $contextfromfile = 0; 00070 public $cattofile = 0; 00071 public $contexttofile = 0; 00072 public $questionids = array(); 00073 public $importerrors = 0; 00074 public $stoponerror = true; 00075 public $translator = null; 00076 public $canaccessbackupdata = true; 00077 00078 protected $importcontext = null; 00079 00080 // functions to indicate import/export functionality 00081 // override to return true if implemented 00082 00084 public function provide_import() { 00085 return false; 00086 } 00087 00089 public function provide_export() { 00090 return false; 00091 } 00092 00094 public function mime_type() { 00095 return mimeinfo('type', $this->export_file_extension()); 00096 } 00097 00102 public function export_file_extension() { 00103 return '.txt'; 00104 } 00105 00106 // Accessor methods 00107 00112 public function setCategory($category) { 00113 if (count($this->questions)) { 00114 debugging('You shouldn\'t call setCategory after setQuestions'); 00115 } 00116 $this->category = $category; 00117 } 00118 00125 public function setQuestions($questions) { 00126 if ($this->category !== null) { 00127 debugging('You shouldn\'t call setQuestions after setCategory'); 00128 } 00129 $this->questions = $questions; 00130 } 00131 00136 public function setCourse($course) { 00137 $this->course = $course; 00138 } 00139 00144 public function setContexts($contexts) { 00145 $this->contexts = $contexts; 00146 $this->translator = new context_to_string_translator($this->contexts); 00147 } 00148 00153 public function setFilename($filename) { 00154 $this->filename = $filename; 00155 } 00156 00162 public function setRealfilename($realfilename) { 00163 $this->realfilename = $realfilename; 00164 } 00165 00170 public function setMatchgrades($matchgrades) { 00171 $this->matchgrades = $matchgrades; 00172 } 00173 00178 public function setCatfromfile($catfromfile) { 00179 $this->catfromfile = $catfromfile; 00180 } 00181 00186 public function setContextfromfile($contextfromfile) { 00187 $this->contextfromfile = $contextfromfile; 00188 } 00189 00194 public function setCattofile($cattofile) { 00195 $this->cattofile = $cattofile; 00196 } 00197 00202 public function setContexttofile($contexttofile) { 00203 $this->contexttofile = $contexttofile; 00204 } 00205 00210 public function setStoponerror($stoponerror) { 00211 $this->stoponerror = $stoponerror; 00212 } 00213 00218 public function set_can_access_backupdata($canaccess) { 00219 $this->canaccessbackupdata = $canaccess; 00220 } 00221 00222 /*********************** 00223 * IMPORTING FUNCTIONS 00224 ***********************/ 00225 00229 protected function error($message, $text='', $questionname='') { 00230 $importerrorquestion = get_string('importerrorquestion', 'question'); 00231 00232 echo "<div class=\"importerror\">\n"; 00233 echo "<strong>$importerrorquestion $questionname</strong>"; 00234 if (!empty($text)) { 00235 $text = s($text); 00236 echo "<blockquote>$text</blockquote>\n"; 00237 } 00238 echo "<strong>$message</strong>\n"; 00239 echo "</div>"; 00240 00241 $this->importerrors++; 00242 } 00243 00253 public function try_importing_using_qtypes($data, $question = null, $extra = null, 00254 $qtypehint = '') { 00255 00256 // work out what format we are using 00257 $formatname = substr(get_class($this), strlen('qformat_')); 00258 $methodname = "import_from_$formatname"; 00259 00260 //first try importing using a hint from format 00261 if (!empty($qtypehint)) { 00262 $qtype = question_bank::get_qtype($qtypehint, false); 00263 if (is_object($qtype) && method_exists($qtype, $methodname)) { 00264 $question = $qtype->$methodname($data, $question, $this, $extra); 00265 if ($question) { 00266 return $question; 00267 } 00268 } 00269 } 00270 00271 // loop through installed questiontypes checking for 00272 // function to handle this question 00273 foreach (question_bank::get_all_qtypes() as $qtype) { 00274 if (method_exists($qtype, $methodname)) { 00275 if ($question = $qtype->$methodname($data, $question, $this, $extra)) { 00276 return $question; 00277 } 00278 } 00279 } 00280 return false; 00281 } 00282 00287 public function importpreprocess() { 00288 return true; 00289 } 00290 00297 public function importprocess($category) { 00298 global $USER, $CFG, $DB, $OUTPUT; 00299 00300 $context = $category->context; 00301 $this->importcontext = $context; 00302 00303 // reset the timer in case file upload was slow 00304 set_time_limit(0); 00305 00306 // STAGE 1: Parse the file 00307 echo $OUTPUT->notification(get_string('parsingquestions', 'question'), 'notifysuccess'); 00308 00309 if (! $lines = $this->readdata($this->filename)) { 00310 echo $OUTPUT->notification(get_string('cannotread', 'question')); 00311 return false; 00312 } 00313 00314 if (!$questions = $this->readquestions($lines, $context)) { // Extract all the questions 00315 echo $OUTPUT->notification(get_string('noquestionsinfile', 'question')); 00316 return false; 00317 } 00318 00319 // STAGE 2: Write data to database 00320 echo $OUTPUT->notification(get_string('importingquestions', 'question', 00321 $this->count_questions($questions)), 'notifysuccess'); 00322 00323 // check for errors before we continue 00324 if ($this->stoponerror and ($this->importerrors>0)) { 00325 echo $OUTPUT->notification(get_string('importparseerror', 'question')); 00326 return true; 00327 } 00328 00329 // get list of valid answer grades 00330 $gradeoptionsfull = question_bank::fraction_options_full(); 00331 00332 // check answer grades are valid 00333 // (now need to do this here because of 'stop on error': MDL-10689) 00334 $gradeerrors = 0; 00335 $goodquestions = array(); 00336 foreach ($questions as $question) { 00337 if (!empty($question->fraction) and (is_array($question->fraction))) { 00338 $fractions = $question->fraction; 00339 $answersvalid = true; // in case they are! 00340 foreach ($fractions as $key => $fraction) { 00341 $newfraction = match_grade_options($gradeoptionsfull, $fraction, 00342 $this->matchgrades); 00343 if ($newfraction === false) { 00344 $answersvalid = false; 00345 } else { 00346 $fractions[$key] = $newfraction; 00347 } 00348 } 00349 if (!$answersvalid) { 00350 echo $OUTPUT->notification(get_string('invalidgrade', 'question')); 00351 ++$gradeerrors; 00352 continue; 00353 } else { 00354 $question->fraction = $fractions; 00355 } 00356 } 00357 $goodquestions[] = $question; 00358 } 00359 $questions = $goodquestions; 00360 00361 // check for errors before we continue 00362 if ($this->stoponerror && $gradeerrors > 0) { 00363 return false; 00364 } 00365 00366 // count number of questions processed 00367 $count = 0; 00368 00369 foreach ($questions as $question) { // Process and store each question 00370 00371 // reset the php timeout 00372 set_time_limit(0); 00373 00374 // check for category modifiers 00375 if ($question->qtype == 'category') { 00376 if ($this->catfromfile) { 00377 // find/create category object 00378 $catpath = $question->category; 00379 $newcategory = $this->create_category_path($catpath); 00380 if (!empty($newcategory)) { 00381 $this->category = $newcategory; 00382 } 00383 } 00384 continue; 00385 } 00386 $question->context = $context; 00387 00388 $count++; 00389 00390 echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>"; 00391 00392 $question->category = $this->category->id; 00393 $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed) 00394 00395 $question->createdby = $USER->id; 00396 $question->timecreated = time(); 00397 $question->modifiedby = $USER->id; 00398 $question->timemodified = time(); 00399 00400 $question->id = $DB->insert_record('question', $question); 00401 if (isset($question->questiontextfiles)) { 00402 foreach ($question->questiontextfiles as $file) { 00403 question_bank::get_qtype($question->qtype)->import_file( 00404 $context, 'question', 'questiontext', $question->id, $file); 00405 } 00406 } 00407 if (isset($question->generalfeedbackfiles)) { 00408 foreach ($question->generalfeedbackfiles as $file) { 00409 question_bank::get_qtype($question->qtype)->import_file( 00410 $context, 'question', 'generalfeedback', $question->id, $file); 00411 } 00412 } 00413 00414 $this->questionids[] = $question->id; 00415 00416 // Now to save all the answers and type-specific options 00417 00418 $result = question_bank::get_qtype($question->qtype)->save_question_options($question); 00419 00420 if (!empty($CFG->usetags) && isset($question->tags)) { 00421 require_once($CFG->dirroot . '/tag/lib.php'); 00422 tag_set('question', $question->id, $question->tags); 00423 } 00424 00425 if (!empty($result->error)) { 00426 echo $OUTPUT->notification($result->error); 00427 return false; 00428 } 00429 00430 if (!empty($result->notice)) { 00431 echo $OUTPUT->notification($result->notice); 00432 return true; 00433 } 00434 00435 // Give the question a unique version stamp determined by question_hash() 00436 $DB->set_field('question', 'version', question_hash($question), 00437 array('id' => $question->id)); 00438 } 00439 return true; 00440 } 00441 00449 protected function count_questions($questions) { 00450 $count = 0; 00451 if (!is_array($questions)) { 00452 return $count; 00453 } 00454 foreach ($questions as $question) { 00455 if (!is_object($question) || !isset($question->qtype) || 00456 ($question->qtype == 'category')) { 00457 continue; 00458 } 00459 $count++; 00460 } 00461 return $count; 00462 } 00463 00475 protected function create_category_path($catpath) { 00476 global $DB; 00477 $catnames = $this->split_category_path($catpath); 00478 $parent = 0; 00479 $category = null; 00480 00481 // check for context id in path, it might not be there in pre 1.9 exports 00482 $matchcount = preg_match('/^\$([a-z]+)\$$/', $catnames[0], $matches); 00483 if ($matchcount == 1) { 00484 $contextid = $this->translator->string_to_context($matches[1]); 00485 array_shift($catnames); 00486 } else { 00487 $contextid = false; 00488 } 00489 00490 if ($this->contextfromfile && $contextid !== false) { 00491 $context = get_context_instance_by_id($contextid); 00492 require_capability('moodle/question:add', $context); 00493 } else { 00494 $context = get_context_instance_by_id($this->category->contextid); 00495 } 00496 00497 // Now create any categories that need to be created. 00498 foreach ($catnames as $catname) { 00499 if ($category = $DB->get_record('question_categories', 00500 array('name' => $catname, 'contextid' => $context->id, 'parent' => $parent))) { 00501 $parent = $category->id; 00502 } else { 00503 require_capability('moodle/question:managecategory', $context); 00504 // create the new category 00505 $category = new stdClass(); 00506 $category->contextid = $context->id; 00507 $category->name = $catname; 00508 $category->info = ''; 00509 $category->parent = $parent; 00510 $category->sortorder = 999; 00511 $category->stamp = make_unique_id_code(); 00512 $id = $DB->insert_record('question_categories', $category); 00513 $category->id = $id; 00514 $parent = $id; 00515 } 00516 } 00517 return $category; 00518 } 00519 00525 protected function readdata($filename) { 00526 if (is_readable($filename)) { 00527 $filearray = file($filename); 00528 00530 if (preg_match("~\r~", $filearray[0]) AND !preg_match("~\n~", $filearray[0])) { 00531 return explode("\r", $filearray[0]); 00532 } else { 00533 return $filearray; 00534 } 00535 } 00536 return false; 00537 } 00538 00552 protected function readquestions($lines, $context) { 00553 00554 $questions = array(); 00555 $currentquestion = array(); 00556 00557 foreach ($lines as $line) { 00558 $line = trim($line); 00559 if (empty($line)) { 00560 if (!empty($currentquestion)) { 00561 if ($question = $this->readquestion($currentquestion)) { 00562 $questions[] = $question; 00563 } 00564 $currentquestion = array(); 00565 } 00566 } else { 00567 $currentquestion[] = $line; 00568 } 00569 } 00570 00571 if (!empty($currentquestion)) { // There may be a final question 00572 if ($question = $this->readquestion($currentquestion, $context)) { 00573 $questions[] = $question; 00574 } 00575 } 00576 00577 return $questions; 00578 } 00579 00587 protected function defaultquestion() { 00588 global $CFG; 00589 static $defaultshuffleanswers = null; 00590 if (is_null($defaultshuffleanswers)) { 00591 $defaultshuffleanswers = get_config('quiz', 'shuffleanswers'); 00592 } 00593 00594 $question = new stdClass(); 00595 $question->shuffleanswers = $defaultshuffleanswers; 00596 $question->defaultmark = 1; 00597 $question->image = ""; 00598 $question->usecase = 0; 00599 $question->multiplier = array(); 00600 $question->questiontextformat = FORMAT_MOODLE; 00601 $question->generalfeedback = ''; 00602 $question->generalfeedbackformat = FORMAT_MOODLE; 00603 $question->correctfeedback = ''; 00604 $question->partiallycorrectfeedback = ''; 00605 $question->incorrectfeedback = ''; 00606 $question->answernumbering = 'abc'; 00607 $question->penalty = 0.3333333; 00608 $question->length = 1; 00609 00610 // this option in case the questiontypes class wants 00611 // to know where the data came from 00612 $question->export_process = true; 00613 $question->import_process = true; 00614 00615 return $question; 00616 } 00617 00628 protected function readquestion($lines) { 00629 00630 $formatnotimplemented = get_string('formatnotimplemented', 'question'); 00631 echo "<p>$formatnotimplemented</p>"; 00632 00633 return null; 00634 } 00635 00640 public function importpostprocess() { 00641 return true; 00642 } 00643 00644 /******************* 00645 * EXPORT FUNCTIONS 00646 *******************/ 00647 00656 protected function try_exporting_using_qtypes($name, $question, $extra=null) { 00657 // work out the name of format in use 00658 $formatname = substr(get_class($this), strlen('qformat_')); 00659 $methodname = "export_to_$formatname"; 00660 00661 $qtype = question_bank::get_qtype($name, false); 00662 if (method_exists($qtype, $methodname)) { 00663 return $qtype->$methodname($question, $this, $extra); 00664 } 00665 return false; 00666 } 00667 00672 public function exportpreprocess() { 00673 return true; 00674 } 00675 00683 protected function presave_process($content) { 00684 return $content; 00685 } 00686 00692 public function exportprocess() { 00693 global $CFG, $OUTPUT, $DB, $USER; 00694 00695 // get the questions (from database) in this category 00696 // only get q's with no parents (no cloze subquestions specifically) 00697 if ($this->category) { 00698 $questions = get_questions_category($this->category, true); 00699 } else { 00700 $questions = $this->questions; 00701 } 00702 00703 $count = 0; 00704 00705 // results are first written into string (and then to a file) 00706 // so create/initialize the string here 00707 $expout = ""; 00708 00709 // track which category questions are in 00710 // if it changes we will record the category change in the output 00711 // file if selected. 0 means that it will get printed before the 1st question 00712 $trackcategory = 0; 00713 00714 // iterate through questions 00715 foreach ($questions as $question) { 00716 // used by file api 00717 $contextid = $DB->get_field('question_categories', 'contextid', 00718 array('id' => $question->category)); 00719 $question->contextid = $contextid; 00720 00721 // do not export hidden questions 00722 if (!empty($question->hidden)) { 00723 continue; 00724 } 00725 00726 // do not export random questions 00727 if ($question->qtype == 'random') { 00728 continue; 00729 } 00730 00731 // check if we need to record category change 00732 if ($this->cattofile) { 00733 if ($question->category != $trackcategory) { 00734 $trackcategory = $question->category; 00735 $categoryname = $this->get_category_path($trackcategory, $this->contexttofile); 00736 00737 // create 'dummy' question for category export 00738 $dummyquestion = new stdClass(); 00739 $dummyquestion->qtype = 'category'; 00740 $dummyquestion->category = $categoryname; 00741 $dummyquestion->name = 'Switch category to ' . $categoryname; 00742 $dummyquestion->id = 0; 00743 $dummyquestion->questiontextformat = ''; 00744 $dummyquestion->contextid = 0; 00745 $expout .= $this->writequestion($dummyquestion) . "\n"; 00746 } 00747 } 00748 00749 // export the question displaying message 00750 $count++; 00751 00752 if (question_has_capability_on($question, 'view', $question->category)) { 00753 $expout .= $this->writequestion($question, $contextid) . "\n"; 00754 } 00755 } 00756 00757 // continue path for following error checks 00758 $course = $this->course; 00759 $continuepath = "$CFG->wwwroot/question/export.php?courseid=$course->id"; 00760 00761 // did we actually process anything 00762 if ($count==0) { 00763 print_error('noquestions', 'question', $continuepath); 00764 } 00765 00766 // final pre-process on exported data 00767 $expout = $this->presave_process($expout); 00768 return $expout; 00769 } 00770 00776 protected function get_category_path($id, $includecontext = true) { 00777 global $DB; 00778 00779 if (!$category = $DB->get_record('question_categories', array('id' => $id))) { 00780 print_error('cannotfindcategory', 'error', '', $id); 00781 } 00782 $contextstring = $this->translator->context_to_string($category->contextid); 00783 00784 $pathsections = array(); 00785 do { 00786 $pathsections[] = $category->name; 00787 $id = $category->parent; 00788 } while ($category = $DB->get_record('question_categories', array('id' => $id))); 00789 00790 if ($includecontext) { 00791 $pathsections[] = '$' . $contextstring . '$'; 00792 } 00793 00794 $path = $this->assemble_category_path(array_reverse($pathsections)); 00795 00796 return $path; 00797 } 00798 00812 protected function assemble_category_path($names) { 00813 $escapednames = array(); 00814 foreach ($names as $name) { 00815 $escapedname = str_replace('/', '//', $name); 00816 if (substr($escapedname, 0, 1) == '/') { 00817 $escapedname = ' ' . $escapedname; 00818 } 00819 if (substr($escapedname, -1) == '/') { 00820 $escapedname = $escapedname . ' '; 00821 } 00822 $escapednames[] = $escapedname; 00823 } 00824 return implode('/', $escapednames); 00825 } 00826 00837 protected function split_category_path($path) { 00838 $rawnames = preg_split('~(?<!/)/(?!/)~', $path); 00839 $names = array(); 00840 foreach ($rawnames as $rawname) { 00841 $names[] = clean_param(trim(str_replace('//', '/', $rawname)), PARAM_MULTILANG); 00842 } 00843 return $names; 00844 } 00845 00850 protected function exportpostprocess() { 00851 return true; 00852 } 00853 00861 protected function writequestion($question) { 00862 // if not overidden, then this is an error. 00863 $formatnotimplemented = get_string('formatnotimplemented', 'question'); 00864 echo "<p>$formatnotimplemented</p>"; 00865 return null; 00866 } 00867 00872 protected function format_question_text($question) { 00873 global $DB; 00874 $formatoptions = new stdClass(); 00875 $formatoptions->noclean = true; 00876 return html_to_text(format_text($question->questiontext, 00877 $question->questiontextformat, $formatoptions), 0, false); 00878 } 00879 }