Moodle  2.2.1
http://www.collinsharper.com
C:/xampp/htdocs/moodle/question/format.php
Go to the documentation of this file.
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 }
 All Data Structures Namespaces Files Functions Variables Enumerations