|
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 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 }