Moodle  2.2.1
http://www.collinsharper.com
C:/xampp/htdocs/moodle/question/type/numerical/questiontype.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 
00017 
00028 defined('MOODLE_INTERNAL') || die();
00029 
00030 require_once($CFG->dirroot . '/question/type/numerical/question.php');
00031 
00032 
00042 class qtype_numerical extends question_type {
00043     const UNITINPUT = 0;
00044     const UNITRADIO = 1;
00045     const UNITSELECT = 2;
00046 
00047     const UNITNONE = 3;
00048     const UNITGRADED = 1;
00049     const UNITOPTIONAL = 0;
00050 
00051     const UNITGRADEDOUTOFMARK = 1;
00052     const UNITGRADEDOUTOFMAX = 2;
00053 
00054     public function get_question_options($question) {
00055         global $CFG, $DB, $OUTPUT;
00056         parent::get_question_options($question);
00057         // Get the question answers and their respective tolerances
00058         // Note: question_numerical is an extension of the answer table rather than
00059         //       the question table as is usually the case for qtype
00060         //       specific tables.
00061         if (!$question->options->answers = $DB->get_records_sql(
00062                                 "SELECT a.*, n.tolerance " .
00063                                 "FROM {question_answers} a, " .
00064                                 "     {question_numerical} n " .
00065                                 "WHERE a.question = ? " .
00066                                 "    AND   a.id = n.answer " .
00067                                 "ORDER BY a.id ASC", array($question->id))) {
00068             echo $OUTPUT->notification('Error: Missing question answer for numerical question ' .
00069                     $question->id . '!');
00070             return false;
00071         }
00072 
00073         $question->hints = $DB->get_records('question_hints',
00074                 array('questionid' => $question->id), 'id ASC');
00075 
00076         $this->get_numerical_units($question);
00077         // get_numerical_options() need to know if there are units
00078         // to set correctly default values
00079         $this->get_numerical_options($question);
00080 
00081         // If units are defined we strip off the default unit from the answer, if
00082         // it is present. (Required for compatibility with the old code and DB).
00083         if ($defaultunit = $this->get_default_numerical_unit($question)) {
00084             foreach ($question->options->answers as $key => $val) {
00085                 $answer = trim($val->answer);
00086                 $length = strlen($defaultunit->unit);
00087                 if ($length && substr($answer, -$length) == $defaultunit->unit) {
00088                     $question->options->answers[$key]->answer =
00089                             substr($answer, 0, strlen($answer)-$length);
00090                 }
00091             }
00092         }
00093 
00094         return true;
00095     }
00096 
00097     public function get_numerical_units(&$question) {
00098         global $DB;
00099 
00100         if ($units = $DB->get_records('question_numerical_units',
00101                 array('question' => $question->id), 'id ASC')) {
00102             $units = array_values($units);
00103         } else {
00104             $units = array();
00105         }
00106         foreach ($units as $key => $unit) {
00107             $units[$key]->multiplier = clean_param($unit->multiplier, PARAM_NUMBER);
00108         }
00109         $question->options->units = $units;
00110         return true;
00111     }
00112 
00113     public function get_default_numerical_unit($question) {
00114         if (isset($question->options->units[0])) {
00115             foreach ($question->options->units as $unit) {
00116                 if (abs($unit->multiplier - 1.0) < '1.0e-' . ini_get('precision')) {
00117                     return $unit;
00118                 }
00119             }
00120         }
00121         return false;
00122     }
00123 
00124     public function get_numerical_options($question) {
00125         global $DB;
00126         if (!$options = $DB->get_record('question_numerical_options',
00127                 array('question' => $question->id))) {
00128             // Old question, set defaults.
00129             $question->options->unitgradingtype = 0;
00130             $question->options->unitpenalty = 0.1;
00131             if ($defaultunit = $this->get_default_numerical_unit($question)) {
00132                 $question->options->showunits = self::UNITINPUT;
00133             } else {
00134                 $question->options->showunits = self::UNITNONE;
00135             }
00136             $question->options->unitsleft = 0;
00137 
00138         } else {
00139             $question->options->unitgradingtype = $options->unitgradingtype;
00140             $question->options->unitpenalty = $options->unitpenalty;
00141             $question->options->showunits = $options->showunits;
00142             $question->options->unitsleft = $options->unitsleft;
00143         }
00144 
00145         return true;
00146     }
00147 
00151     public function save_question_options($question) {
00152         global $DB;
00153         $context = $question->context;
00154 
00155         // Get old versions of the objects
00156         $oldanswers = $DB->get_records('question_answers',
00157                 array('question' => $question->id), 'id ASC');
00158         $oldoptions = $DB->get_records('question_numerical',
00159                 array('question' => $question->id), 'answer ASC');
00160 
00161         // Save the units.
00162         $result = $this->save_units($question);
00163         if (isset($result->error)) {
00164             return $result;
00165         } else {
00166             $units = $result->units;
00167         }
00168 
00169         // Insert all the new answers
00170         foreach ($question->answer as $key => $answerdata) {
00171             // Check for, and ingore, completely blank answer from the form.
00172             if (trim($answerdata) == '' && $question->fraction[$key] == 0 &&
00173                     html_is_blank($question->feedback[$key]['text'])) {
00174                 continue;
00175             }
00176 
00177             // Update an existing answer if possible.
00178             $answer = array_shift($oldanswers);
00179             if (!$answer) {
00180                 $answer = new stdClass();
00181                 $answer->question = $question->id;
00182                 $answer->answer = '';
00183                 $answer->feedback = '';
00184                 $answer->id = $DB->insert_record('question_answers', $answer);
00185             }
00186 
00187             if (trim($answerdata) === '*') {
00188                 $answer->answer = '*';
00189             } else {
00190                 $answer->answer = $this->apply_unit($answerdata, $units,
00191                         !empty($question->unitsleft));
00192                 if ($answer->answer === false) {
00193                     $result->notice = get_string('invalidnumericanswer', 'quiz');
00194                 }
00195             }
00196             $answer->fraction = $question->fraction[$key];
00197             $answer->feedback = $this->import_or_save_files($question->feedback[$key],
00198                     $context, 'question', 'answerfeedback', $answer->id);
00199             $answer->feedbackformat = $question->feedback[$key]['format'];
00200             $DB->update_record('question_answers', $answer);
00201 
00202             // Set up the options object
00203             if (!$options = array_shift($oldoptions)) {
00204                 $options = new stdClass();
00205             }
00206             $options->question = $question->id;
00207             $options->answer   = $answer->id;
00208             if (trim($question->tolerance[$key]) == '') {
00209                 $options->tolerance = '';
00210             } else {
00211                 $options->tolerance = $this->apply_unit($question->tolerance[$key],
00212                         $units, !empty($question->unitsleft));
00213                 if ($options->tolerance === false) {
00214                     $result->notice = get_string('invalidnumerictolerance', 'quiz');
00215                 }
00216             }
00217             if (isset($options->id)) {
00218                 $DB->update_record('question_numerical', $options);
00219             } else {
00220                 $DB->insert_record('question_numerical', $options);
00221             }
00222         }
00223 
00224         // Delete any left over old answer records.
00225         $fs = get_file_storage();
00226         foreach ($oldanswers as $oldanswer) {
00227             $fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id);
00228             $DB->delete_records('question_answers', array('id' => $oldanswer->id));
00229         }
00230         foreach ($oldoptions as $oldoption) {
00231             $DB->delete_records('question_numerical', array('id' => $oldoption->id));
00232         }
00233 
00234         $result = $this->save_unit_options($question);
00235         if (!empty($result->error) || !empty($result->notice)) {
00236             return $result;
00237         }
00238 
00239         $this->save_hints($question);
00240 
00241         return true;
00242     }
00243 
00252     public function save_unit_options($question) {
00253         global $DB;
00254         $result = new stdClass();
00255 
00256         $update = true;
00257         $options = $DB->get_record('question_numerical_options',
00258                 array('question' => $question->id));
00259         if (!$options) {
00260             $options = new stdClass();
00261             $options->question = $question->id;
00262             $options->id = $DB->insert_record('question_numerical_options', $options);
00263         }
00264 
00265         if (isset($question->unitpenalty)) {
00266             $options->unitpenalty = $question->unitpenalty;
00267         } else {
00268             // Either an old question or a close question type.
00269             $options->unitpenalty = 1;
00270         }
00271 
00272         $options->unitgradingtype = 0;
00273         if (isset($question->unitrole)) {
00274             // Saving the editing form.
00275             $options->showunits = $question->unitrole;
00276             if ($question->unitrole == self::UNITGRADED) {
00277                 $options->unitgradingtype = $question->unitgradingtypes;
00278                 $options->showunits = $question->multichoicedisplay;
00279             }
00280 
00281         } else if (isset($question->showunits)) {
00282             // Updated import, e.g. Moodle XML.
00283             $options->showunits = $question->showunits;
00284 
00285         } else {
00286             // Legacy import.
00287             if ($defaultunit = $this->get_default_numerical_unit($question)) {
00288                 $options->showunits = self::UNITINPUT;
00289             } else {
00290                 $options->showunits = self::UNITNONE;
00291             }
00292         }
00293 
00294         $options->unitsleft = !empty($question->unitsleft);
00295 
00296         $DB->update_record('question_numerical_options', $options);
00297 
00298         // Report any problems.
00299         if (!empty($result->notice)) {
00300             return $result;
00301         }
00302 
00303         return true;
00304     }
00305 
00306     public function save_units($question) {
00307         global $DB;
00308         $result = new stdClass();
00309 
00310         // Delete the units previously saved for this question.
00311         $DB->delete_records('question_numerical_units', array('question' => $question->id));
00312 
00313         // Nothing to do.
00314         if (!isset($question->multiplier)) {
00315             $result->units = array();
00316             return $result;
00317         }
00318 
00319         // Save the new units.
00320         $units = array();
00321         $unitalreadyinsert = array();
00322         foreach ($question->multiplier as $i => $multiplier) {
00323             // Discard any unit which doesn't specify the unit or the multiplier
00324             if (!empty($question->multiplier[$i]) && !empty($question->unit[$i]) &&
00325                     !array_key_exists($question->unit[$i], $unitalreadyinsert)) {
00326                 $unitalreadyinsert[$question->unit[$i]] = 1;
00327                 $units[$i] = new stdClass();
00328                 $units[$i]->question = $question->id;
00329                 $units[$i]->multiplier = $this->apply_unit($question->multiplier[$i],
00330                         array(), false);
00331                 $units[$i]->unit = $question->unit[$i];
00332                 $DB->insert_record('question_numerical_units', $units[$i]);
00333             }
00334         }
00335         unset($question->multiplier, $question->unit);
00336 
00337         $result->units = &$units;
00338         return $result;
00339     }
00340 
00341     protected function initialise_question_instance(question_definition $question, $questiondata) {
00342         parent::initialise_question_instance($question, $questiondata);
00343         $this->initialise_numerical_answers($question, $questiondata);
00344         $question->unitdisplay = $questiondata->options->showunits;
00345         $question->unitgradingtype = $questiondata->options->unitgradingtype;
00346         $question->unitpenalty = $questiondata->options->unitpenalty;
00347         $question->ap = $this->make_answer_processor($questiondata->options->units,
00348                 $questiondata->options->unitsleft);
00349     }
00350 
00351     public function initialise_numerical_answers(question_definition $question, $questiondata) {
00352         $question->answers = array();
00353         if (empty($questiondata->options->answers)) {
00354             return;
00355         }
00356         foreach ($questiondata->options->answers as $a) {
00357             $question->answers[$a->id] = new qtype_numerical_answer($a->id, $a->answer,
00358                     $a->fraction, $a->feedback, $a->feedbackformat, $a->tolerance);
00359         }
00360     }
00361 
00362     public function make_answer_processor($units, $unitsleft) {
00363         if (empty($units)) {
00364             return new qtype_numerical_answer_processor(array());
00365         }
00366 
00367         $cleanedunits = array();
00368         foreach ($units as $unit) {
00369             $cleanedunits[$unit->unit] = $unit->multiplier;
00370         }
00371 
00372         return new qtype_numerical_answer_processor($cleanedunits, $unitsleft);
00373     }
00374 
00375     public function delete_question($questionid, $contextid) {
00376         global $DB;
00377         $DB->delete_records('question_numerical', array('question' => $questionid));
00378         $DB->delete_records('question_numerical_options', array('question' => $questionid));
00379         $DB->delete_records('question_numerical_units', array('question' => $questionid));
00380 
00381         parent::delete_question($questionid, $contextid);
00382     }
00383 
00384     public function get_random_guess_score($questiondata) {
00385         foreach ($questiondata->options->answers as $aid => $answer) {
00386             if ('*' == trim($answer->answer)) {
00387                 return max($answer->fraction - $questiondata->options->unitpenalty, 0);
00388             }
00389         }
00390         return 0;
00391     }
00392 
00400     public function add_unit($questiondata, $answer, $unit = null) {
00401         if (is_null($unit)) {
00402             $unit = $this->get_default_numerical_unit($questiondata);
00403         }
00404 
00405         if (!$unit) {
00406             return $answer;
00407         }
00408 
00409         if (!empty($questiondata->options->unitsleft)) {
00410             return $unit->unit . ' ' . $answer;
00411         } else {
00412             return $answer . ' ' . $unit->unit;
00413         }
00414     }
00415 
00416     public function get_possible_responses($questiondata) {
00417         $responses = array();
00418 
00419         $unit = $this->get_default_numerical_unit($questiondata);
00420 
00421         $starfound = false;
00422         foreach ($questiondata->options->answers as $aid => $answer) {
00423             $responseclass = $answer->answer;
00424 
00425             if ($responseclass === '*') {
00426                 $starfound = true;
00427             } else {
00428                 $responseclass = $this->add_unit($questiondata, $responseclass, $unit);
00429 
00430                 $ans = new qtype_numerical_answer($answer->id, $answer->answer, $answer->fraction,
00431                         $answer->feedback, $answer->feedbackformat, $answer->tolerance);
00432                 list($min, $max) = $ans->get_tolerance_interval();
00433                 $responseclass .= " ($min..$max)";
00434             }
00435 
00436             $responses[$aid] = new question_possible_response($responseclass,
00437                     $answer->fraction);
00438         }
00439 
00440         if (!$starfound) {
00441             $responses[0] = new question_possible_response(
00442                     get_string('didnotmatchanyanswer', 'question'), 0);
00443         }
00444 
00445         $responses[null] = question_possible_response::no_response();
00446 
00447         return array($questiondata->id => $responses);
00448     }
00449 
00459     public function apply_unit($rawresponse, $units, $unitsleft) {
00460         $ap = $this->make_answer_processor($units, $unitsleft);
00461         list($value, $unit, $multiplier) = $ap->apply_units($rawresponse);
00462         if (!is_null($multiplier)) {
00463             $value *= $multiplier;
00464         }
00465         return $value;
00466     }
00467 
00468     public function move_files($questionid, $oldcontextid, $newcontextid) {
00469         $fs = get_file_storage();
00470 
00471         parent::move_files($questionid, $oldcontextid, $newcontextid);
00472         $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid);
00473     }
00474 
00475     protected function delete_files($questionid, $contextid) {
00476         $fs = get_file_storage();
00477 
00478         parent::delete_files($questionid, $contextid);
00479         $this->delete_files_in_answers($questionid, $contextid);
00480     }
00481 }
00482 
00483 
00490 class qtype_numerical_answer_processor {
00492     protected $units;
00494     protected $decsep;
00496     protected $thousandssep;
00498     protected $unitsbefore;
00499 
00500     protected $regex = null;
00501 
00502     public function __construct($units, $unitsbefore = false, $decsep = null,
00503             $thousandssep = null) {
00504         if (is_null($decsep)) {
00505             $decsep = get_string('decsep', 'langconfig');
00506         }
00507         $this->decsep = $decsep;
00508 
00509         if (is_null($thousandssep)) {
00510             $thousandssep = get_string('thousandssep', 'langconfig');
00511         }
00512         $this->thousandssep = $thousandssep;
00513 
00514         $this->units = $units;
00515         $this->unitsbefore = $unitsbefore;
00516     }
00517 
00523     public function set_characters($decsep, $thousandssep) {
00524         $this->decsep = $decsep;
00525         $this->thousandssep = $thousandssep;
00526         $this->regex = null;
00527     }
00528 
00530     public function get_point() {
00531         return $this->decsep;
00532     }
00533 
00535     public function get_separator() {
00536         return $this->thousandssep;
00537     }
00538 
00544     public function contains_thousands_seaparator($value) {
00545         if (!in_array($this->thousandssep, array('.', ','))) {
00546             return false;
00547         }
00548 
00549         return strpos($value, $this->thousandssep) !== false;
00550     }
00551 
00556     protected function build_regex() {
00557         if (!is_null($this->regex)) {
00558             return $this->regex;
00559         }
00560 
00561         $decsep = preg_quote($this->decsep, '/');
00562         $thousandssep = preg_quote($this->thousandssep, '/');
00563         $beforepointre = '([+-]?[' . $thousandssep . '\d]*)';
00564         $decimalsre = $decsep . '(\d*)';
00565         $exponentre = '(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)';
00566 
00567         $numberbit = "$beforepointre(?:$decimalsre)?(?:$exponentre)?";
00568 
00569         if ($this->unitsbefore) {
00570             $this->regex = "/$numberbit$/";
00571         } else {
00572             $this->regex = "/^$numberbit/";
00573         }
00574         return $this->regex;
00575     }
00576 
00590     protected function parse_response($response) {
00591         if (!preg_match($this->build_regex(), $response, $matches)) {
00592             return array(null, null, null, null);
00593         }
00594 
00595         $matches += array('', '', '', ''); // Fill in any missing matches.
00596         list($matchedpart, $beforepoint, $decimals, $exponent) = $matches;
00597 
00598         // Strip out thousands separators.
00599         $beforepoint = str_replace($this->thousandssep, '', $beforepoint);
00600 
00601         // Must be either something before, or something after the decimal point.
00602         // (The only way to do this in the regex would make it much more complicated.)
00603         if ($beforepoint === '' && $decimals === '') {
00604             return array(null, null, null, null);
00605         }
00606 
00607         if ($this->unitsbefore) {
00608             $unit = substr($response, 0, -strlen($matchedpart));
00609         } else {
00610             $unit = substr($response, strlen($matchedpart));
00611         }
00612         $unit = trim($unit);
00613 
00614         return array($beforepoint, $decimals, $exponent, $unit);
00615     }
00616 
00626     public function apply_units($response, $separateunit = null) {
00627         // Strip spaces (which may be thousands separators) and change other forms
00628         // of writing e to e.
00629         $response = str_replace(' ', '', $response);
00630         $response = preg_replace('~(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)~', 'e$1', $response);
00631 
00632         // If a . is present or there are multiple , (i.e. 2,456,789 ) assume ,
00633         // is a thouseands separator, and strip it, else assume it is a decimal
00634         // separator, and change it to ..
00635         if (strpos($response, '.') !== false || substr_count($response, ',') > 1) {
00636             $response = str_replace(',', '', $response);
00637         } else {
00638             $response = str_replace(',', '.', $response);
00639         }
00640 
00641         $regex = '[+-]?(?:\d+(?:\\.\d*)?|\\.\d+)(?:e[-+]?\d+)?';
00642         if ($this->unitsbefore) {
00643             $regex = "/$regex$/";
00644         } else {
00645             $regex = "/^$regex/";
00646         }
00647         if (!preg_match($regex, $response, $matches)) {
00648             return array(null, null, null);
00649         }
00650 
00651         $numberstring = $matches[0];
00652         if ($this->unitsbefore) {
00653             // substr returns false when it means '', so cast back to string.
00654             $unit = (string) substr($response, 0, -strlen($numberstring));
00655         } else {
00656             $unit = (string) substr($response, strlen($numberstring));
00657         }
00658 
00659         if (!is_null($separateunit)) {
00660             $unit = $separateunit;
00661         }
00662 
00663         if ($this->is_known_unit($unit)) {
00664             $multiplier = 1 / $this->units[$unit];
00665         } else {
00666             $multiplier = null;
00667         }
00668 
00669         return array($numberstring + 0, $unit, $multiplier); // + 0 to convert to number.
00670     }
00671 
00675     public function get_default_unit() {
00676         reset($this->units);
00677         return key($this->units);
00678     }
00679 
00684     public function add_unit($answer, $unit = null) {
00685         if (is_null($unit)) {
00686             $unit = $this->get_default_unit();
00687         }
00688 
00689         if (!$unit) {
00690             return $answer;
00691         }
00692 
00693         if ($this->unitsbefore) {
00694             return $unit . ' ' . $answer;
00695         } else {
00696             return $answer . ' ' . $unit;
00697         }
00698     }
00699 
00705     public function is_known_unit($unit) {
00706         return array_key_exists($unit, $this->units);
00707     }
00708 
00713     public function are_units_before() {
00714         return $this->unitsbefore;
00715     }
00716 
00721     public function get_unit_options() {
00722         $options = array();
00723         foreach ($this->units as $unit => $notused) {
00724             $options[$unit] = $unit;
00725         }
00726         return $options;
00727     }
00728 }
 All Data Structures Namespaces Files Functions Variables Enumerations