"MDL-13766, use file_storage to bypass capability check for recent plugin"
[moodle.git] / question / type / numerical / questiontype.php
CommitLineData
aeb15530 1<?php
1fe641f7 2/**
1fe641f7 3 * @author Martin Dougiamas and many others. Tim Hunt.
4 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
1976496e 5 * @package questionbank
6 * @subpackage questiontypes
1fe641f7 7 *//** */
516cf3eb 8
aaae75b0 9require_once("$CFG->dirroot/question/type/shortanswer/questiontype.php");
516cf3eb 10
1fe641f7 11/**
12 * NUMERICAL QUESTION TYPE CLASS
13 *
14 * This class contains some special features in order to make the
15 * question type embeddable within a multianswer (cloze) question
16 *
17 * This question type behaves like shortanswer in most cases.
18 * Therefore, it extends the shortanswer question type...
1976496e 19 * @package questionbank
20 * @subpackage questiontypes
1fe641f7 21 */
32a189d6 22class question_numerical_qtype extends question_shortanswer_qtype {
516cf3eb 23
c5da9906 24 public $virtualqtype = false;
25 public $unitpenalty = 0;
26 public $raw_unitgrade = 0 ;
27 public $raw_unitpenalty = 0 ;
28 public $valid_numerical_unit = true ;
516cf3eb 29 function name() {
30 return 'numerical';
31 }
aeb15530 32
869309b8 33 function has_wildcards_in_responses() {
34 return true;
35 }
36
37 function requires_qtypes() {
38 return array('shortanswer');
39 }
516cf3eb 40
41 function get_question_options(&$question) {
42 // Get the question answers and their respective tolerances
32a189d6 43 // Note: question_numerical is an extension of the answer table rather than
4f48fb42 44 // the question table as is usually the case for qtype
516cf3eb 45 // specific tables.
fef8f84e 46 global $CFG, $DB, $OUTPUT;
c5da9906 47 $this->get_numerical_options($question);
f34488b2 48 if (!$question->options->answers = $DB->get_records_sql(
516cf3eb 49 "SELECT a.*, n.tolerance " .
f34488b2 50 "FROM {question_answers} a, " .
51 " {question_numerical} n " .
52 "WHERE a.question = ? " .
026bec73 53 " AND a.id = n.answer " .
f34488b2 54 "ORDER BY a.id ASC", array($question->id))) {
fef8f84e 55 echo $OUTPUT->notification('Error: Missing question answer for numerical question ' . $question->id . '!');
516cf3eb 56 return false;
57 }
58 $this->get_numerical_units($question);
59
5a14d563 60 // If units are defined we strip off the default unit from the answer, if
516cf3eb 61 // it is present. (Required for compatibility with the old code and DB).
62 if ($defaultunit = $this->get_default_numerical_unit($question)) {
63 foreach($question->options->answers as $key => $val) {
64 $answer = trim($val->answer);
65 $length = strlen($defaultunit->unit);
1fe641f7 66 if ($length && substr($answer, -$length) == $defaultunit->unit) {
516cf3eb 67 $question->options->answers[$key]->answer =
1fe641f7 68 substr($answer, 0, strlen($answer)-$length);
516cf3eb 69 }
70 }
71 }
c5da9906 72
73 return true;
74 }
75 function get_numerical_options(&$question) {
76 global $DB;
77 if (!$options = $DB->get_record('question_numerical_options', array('question' => $question->id))) {
78 // echo $OUTPUT->notification('Error: Missing question options for numerical question'.$question->id.'!');
79 // return false;
80 $question->options->unitgradingtype = 0;
81 $question->options->unitpenalty = 0;
82 $question->options->showunits = 0 ;
83 $question->options->unitsleft = 0 ;
84 $question->options->instructions = '' ;
85 } else {
86 $question->options->unitgradingtype = $options->unitgradingtype;
87 $question->options->unitpenalty = $options->unitpenalty;
88 $question->options->showunits = $options->showunits ;
89 $question->options->unitsleft = $options->unitsleft ;
90 $question->options->instructions = $options->instructions ;
91 }
aeb15530
PS
92
93
516cf3eb 94 return true;
95 }
516cf3eb 96 function get_numerical_units(&$question) {
f34488b2 97 global $DB;
98 if ($units = $DB->get_records('question_numerical_units', array('question' => $question->id), 'id ASC')) {
3a513ba4 99 $units = array_values($units);
516cf3eb 100 } else {
b4d7d27c 101 $units = array();
516cf3eb 102 }
b4d7d27c 103 foreach ($units as $key => $unit) {
104 $units[$key]->multiplier = clean_param($unit->multiplier, PARAM_NUMBER);
105 }
106 $question->options->units = $units;
516cf3eb 107 return true;
108 }
109
110 function get_default_numerical_unit(&$question) {
ed0ba6da 111 if (isset($question->options->units[0])) {
112 foreach ($question->options->units as $unit) {
113 if (abs($unit->multiplier - 1.0) < '1.0e-' . ini_get('precision')) {
114 return $unit;
516cf3eb 115 }
116 }
117 }
ed0ba6da 118 return false;
516cf3eb 119 }
120
1fe641f7 121 /**
122 * Save the units and the answers associated with this question.
123 */
516cf3eb 124 function save_question_options($question) {
f34488b2 125 global $DB;
516cf3eb 126 // Get old versions of the objects
f34488b2 127 if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) {
516cf3eb 128 $oldanswers = array();
129 }
130
f34488b2 131 if (!$oldoptions = $DB->get_records('question_numerical', array('question' => $question->id), 'answer ASC')) {
516cf3eb 132 $oldoptions = array();
133 }
134
1fe641f7 135 // Save the units.
516cf3eb 136 $result = $this->save_numerical_units($question);
137 if (isset($result->error)) {
138 return $result;
139 } else {
140 $units = &$result->units;
141 }
142
143 // Insert all the new answers
144 foreach ($question->answer as $key => $dataanswer) {
94a6d656 145 // Check for, and ingore, completely blank answer from the form.
146 if (trim($dataanswer) == '' && $question->fraction[$key] == 0 &&
147 html_is_blank($question->feedback[$key])) {
148 continue;
149 }
516cf3eb 150
94a6d656 151 $answer = new stdClass;
152 $answer->question = $question->id;
fac1189d 153 if (trim($dataanswer) === '*') {
94a6d656 154 $answer->answer = '*';
155 } else {
156 $answer->answer = $this->apply_unit($dataanswer, $units);
157 if ($answer->answer === false) {
158 $result->notice = get_string('invalidnumericanswer', 'quiz');
516cf3eb 159 }
94a6d656 160 }
161 $answer->fraction = $question->fraction[$key];
162 $answer->feedback = trim($question->feedback[$key]);
516cf3eb 163
94a6d656 164 if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it
165 $answer->id = $oldanswer->id;
0bcf8b6f 166 $DB->update_record("question_answers", $answer);
94a6d656 167 } else { // This is a completely new answer
0bcf8b6f 168 $answer->id = $DB->insert_record("question_answers", $answer);
94a6d656 169 }
f34488b2 170
94a6d656 171 // Set up the options object
172 if (!$options = array_shift($oldoptions)) {
173 $options = new stdClass;
174 }
175 $options->question = $question->id;
176 $options->answer = $answer->id;
177 if (trim($question->tolerance[$key]) == '') {
178 $options->tolerance = '';
179 } else {
180 $options->tolerance = $this->apply_unit($question->tolerance[$key], $units);
181 if ($options->tolerance === false) {
182 $result->notice = get_string('invalidnumerictolerance', 'quiz');
183 }
184 }
185
186 // Save options
187 if (isset($options->id)) { // reusing existing record
0bcf8b6f 188 $DB->update_record('question_numerical', $options);
94a6d656 189 } else { // new options
0bcf8b6f 190 $DB->insert_record('question_numerical', $options);
1fe641f7 191 }
192 }
193 // delete old answer records
194 if (!empty($oldanswers)) {
195 foreach($oldanswers as $oa) {
f34488b2 196 $DB->delete_records('question_answers', array('id' => $oa->id));
1fe641f7 197 }
198 }
516cf3eb 199
1fe641f7 200 // delete old answer records
201 if (!empty($oldoptions)) {
202 foreach($oldoptions as $oo) {
f34488b2 203 $DB->delete_records('question_numerical', array('id' => $oo->id));
516cf3eb 204 }
205 }
c5da9906 206 $result = $this->save_numerical_options($question);
207 if (isset($result->error)) {
208 return $result;
209 }
1fe641f7 210 // Report any problems.
211 if (!empty($result->notice)) {
212 return $result;
213 }
1fe641f7 214 return true;
516cf3eb 215 }
216
c5da9906 217 function save_numerical_options(&$question) {
218 global $DB;
219 $result = new stdClass;
220 // numerical options
aeb15530 221 $update = true ;
c5da9906 222 $options = $DB->get_record("question_numerical_options", array("question" => $question->id));
223 if (!$options) {
224 $update = false;
225 $options = new stdClass;
226 $options->question = $question->id;
227 }
228 if(isset($question->unitgradingtype)){
229 $options->unitgradingtype = $question->unitgradingtype;
230 }else {
231 $options->unitgradingtype = 0 ;
232 }
233 if(isset($question->unitpenalty)){
234 $options->unitpenalty = $question->unitpenalty;
235 }else {
236 $options->unitpenalty = 0 ;
237 }
238 if(isset($question->showunits)){
239 $options->showunits = $question->showunits;
240 }else {
241 $options->showunits = 0 ;
242 }
243 if(isset($question->unitsleft)){
244 $options->unitsleft = $question->unitsleft;
245 }else {
246 $options->unitsleft = 0 ;
247 }
248 if(isset($question->instructions)){
249 $options->instructions = trim($question->instructions);
250 }else {
251 $options->instructions = '' ;
252 }
253 if ($update) {
254 if (!$DB->update_record("question_numerical_options", $options)) {
255 $result->error = "Could not update numerical question options! (id=$options->id)";
256 return $result;
257 }
258 } else {
259 if (!$DB->insert_record("question_numerical_options", $options)) {
260 $result->error = "Could not insert numerical question options!";
261 return $result;
262 }
263 }
264 return $result;
265 }
266
516cf3eb 267 function save_numerical_units($question) {
f34488b2 268 global $DB;
ed0ba6da 269 $result = new stdClass;
516cf3eb 270
ed0ba6da 271 // Delete the units previously saved for this question.
f34488b2 272 $DB->delete_records('question_numerical_units', array('question' => $question->id));
ed0ba6da 273
26b26662 274 // Nothing to do.
275 if (!isset($question->multiplier)) {
276 $result->units = array();
277 return $result;
278 }
279
ed0ba6da 280 // Save the new units.
516cf3eb 281 $units = array();
ed0ba6da 282 foreach ($question->multiplier as $i => $multiplier) {
516cf3eb 283 // Discard any unit which doesn't specify the unit or the multiplier
284 if (!empty($question->multiplier[$i]) && !empty($question->unit[$i])) {
ed0ba6da 285 $units[$i] = new stdClass;
516cf3eb 286 $units[$i]->question = $question->id;
1fe641f7 287 $units[$i]->multiplier = $this->apply_unit($question->multiplier[$i], array());
516cf3eb 288 $units[$i]->unit = $question->unit[$i];
979425b5 289 $DB->insert_record('question_numerical_units', $units[$i]);
516cf3eb 290 }
291 }
292 unset($question->multiplier, $question->unit);
293
516cf3eb 294 $result->units = &$units;
295 return $result;
296 }
297
c5da9906 298 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
299 $state->responses = array();
300 $state->responses['answer'] = '';
301 $state->responses['unit'] = '';
302 /* if ($question->options->showunits == 1){
303 $state->responses['unit'] = '0';
304 }*/
aeb15530 305
c5da9906 306 return true;
307 }
308 function restore_session_and_responses(&$question, &$state) {
309 if(false === strpos($state->responses[''], '|||||')){
310 // temporary
aeb15530 311 $state->responses['answer']= $state->responses[''];
c5da9906 312 $state->responses['unit'] = '';
1c5299bf 313 $this->split_old_answer($state->responses[''], $question->options->units, $state->responses['answer'] ,$state->responses['unit'] );
c5da9906 314 }else {
315 $responses = explode('|||||', $state->responses['']);
316 $state->responses['answer']= $responses[0];
317 $state->responses['unit'] = $responses[1];
318 }
319 // echo "<p> restore response $responses || <pre>";print_r($state);echo "</pre></p>";
320
aeb15530 321 /*
c5da9906 322 if ($question->options->showunits == 1 && isset($question->options->units)){
323 $state->responses['unit']=$this->find_unit_index($question,$state->responses['unit']);
324 }*/
325 return true;
326 }
327
328 function find_unit_index(&$question,$value){
329 $length = 0;
330 $goodkey = 0 ;
331 foreach ($question->options->units as $key => $unit){
332 if($unit->unit ==$value ) {
333 return $key ;
334 }
aeb15530 335 }
c5da9906 336 return 0 ;
337 }
338
339 function split_old_answer($rawresponse, $units, &$answer ,&$unit ) {
340 $answer = $rawresponse ;
341 // remove spaces and normalise decimal places.
342 $search = array(' ', ',');
343 $replace = array('', '.');
344 $rawresponse = str_replace($search, $replace, trim($rawresponse));
345 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
346 $rawresponse, $responseparts)) {
347 $unit = $responseparts[5] ;
348 $answer = $responseparts[1] ;
349 }
350 return ;
351 }
352
353
354 function save_session_and_responses(&$question, &$state) {
355 global $DB;
356 // echo "<p> save session <pre>";print_r($state);echo "</pre></p>";
357
358 $responses = '';
359 if(isset($state->responses['unit']) && isset($question->options->units[$state->responses['unit']])){
360 $responses = $state->responses['answer'].'|||||'.$question->options->units[$state->responses['unit']]->unit;
361 }else if(isset($state->responses['unit'])){
362 $responses = $state->responses['answer'].'|||||'.$state->responses['unit'] ;
363 }else {
364 $responses = $state->responses['answer'].'|||||';
365 }
366 // Set the legacy answer field
367 if (!$DB->set_field('question_states', 'answer', $responses, array('id' => $state->id))) {
368 return false;
369 }
370 return true;
371 }
372
373/**
1fe641f7 374 * Deletes question from the question-type specific tables
375 *
376 * @return boolean Success/Failure
377 * @param object $question The question being deleted
378 */
90c3f310 379 function delete_question($questionid) {
f34488b2 380 global $DB;
381 $DB->delete_records("question_numerical", array("question" => $questionid));
c5da9906 382 $DB->delete_records("question_numerical_options", array("question" => $questionid));
f34488b2 383 $DB->delete_records("question_numerical_units", array("question" => $questionid));
516cf3eb 384 return true;
385 }
aeb15530 386
516cf3eb 387
5a14d563 388 function compare_responses(&$question, $state, $teststate) {
c5da9906 389 if ($question->options->showunits == 1 && isset($question->options->units) && isset($question->options->units[$state->responses['unit']] )){
390 $state->responses['unit']=$question->options->units[$state->responses['unit']]->unit;
391 };
392
aeb15530 393
c5da9906 394 $responses = '';
395 $testresponses = '';
396 if (isset($state->responses['answer'])){
397 $responses = $state->responses['answer'];
398 }
399 if (isset($state->responses['unit'])){
400 $responses .= $state->responses['unit'];
401 }
402 if (isset($teststate->responses['answer'])){
403 $testresponses = $teststate->responses['answer'];
404 }
405 if (isset($teststate->responses['unit'])){
406 $testresponses .= $teststate->responses['unit'];
407 }
8b831fbb
PP
408 // echo "<p> compare response $responses || $testresponses <pre>";print_r($state);echo "</pre></p>";
409
f0b6151c 410 if ( isset($responses) && isset($testresponses )) {
c5da9906 411
412 return $responses == $testresponses ;
5a14d563 413 }
414 return false;
516cf3eb 415 }
416
1fe641f7 417 /**
418 * Checks whether a response matches a given answer, taking the tolerance
419 * and units into account. Returns a true for if a response matches the
420 * answer, false if it doesn't.
421 */
516cf3eb 422 function test_response(&$question, &$state, $answer) {
55894a42 423 // Deal with the match anything answer.
fac1189d 424 if ($answer->answer === '*') {
55894a42 425 return true;
516cf3eb 426 }
f0b6151c 427 // echo "<p> test response numerical state <pre>";print_r($state);echo "</pre></p>";
c5da9906 428 // echo "<p> test response numerical question <pre>";print_r($question);echo "</pre></p>";
429
430 if( isset($state->responses['unit']) && isset($question->options->units[$state->responses['unit']])){
431 $state->responses['']=$state->responses['answer'].$question->options->units[$state->responses['unit']]->unit;
432 // echo "<p>test responses valid unit </p>";
433 }else if(isset($state->responses['unit'])){
434 $state->responses['']= $state->responses['answer'].$state->responses['unit'] ;// why?
435 }
436 // echo "<p> test response numerical state before apply <pre>";print_r($state);echo "</pre></p>";
aeb15530 437
294ce987 438 $response = $this->apply_unit($state->responses[''], $question->options->units);
c5da9906 439 // $this->valid_numerical_unit = $this->valid_unit($state->responses[''], $question->options->units);
aeb15530
PS
440 // if ($this->valid_numerical_unit) echo "<p>test responses valid unit </p>";
441 // if (!$this->valid_numerical_unit) echo "<p>test responses not valid unit </p>";
c5da9906 442 // echo "<p>state response test $response <pre>";print_r($state);echo "</pre></p>";
443 // $this->raw_unitpenalty = 0.1 ;
1fe641f7 444
445 if ($response === false) {
446 return false; // The student did not type a number.
516cf3eb 447 }
1fe641f7 448
449 // The student did type a number, so check it with tolerances.
450 $this->get_tolerance_interval($answer);
451 return ($answer->min <= $response && $response <= $answer->max);
516cf3eb 452 }
453
c5da9906 454 function grade_responses(&$question, &$state, $cmoptions) {
455 // The default implementation uses the test_response method to
456 // compare what the student entered against each of the possible
457 // answers stored in the question, and uses the grade from the
458 // first one that matches. It also sets the marks and penalty.
459 // This should be good enought for most simple question types.
460 // echo "<p>grade responses <pre>";print_r($state->responses);echo "</pre></p>";
aeb15530
PS
461
462 //first the split response from unit choice display is converted as
c5da9906 463 // standard numerical response value.unit
464 /* if (!empty($question->options->showunits) && isset($state->responses['unit'])){
465 $state->responses[''] .= $question->options->units[$state->responses['unit']]->unit ;
466 }
aeb15530 467 */
c5da9906 468 //to apply the unit penalty we need to analyse the response in a more complex way
469 //the apply_unit() function analysis could be used to obtain the infos
aeb15530
PS
470 // however it is used to detect good or bad numbers but also
471 // gives false
c5da9906 472 $state->raw_grade = 0;
473 foreach($question->options->answers as $answer) {
474 if($this->test_response($question, $state, $answer)) {
475 $state->raw_grade = $answer->fraction;
476 $this->raw_unitgrade = $answer->fraction;
477 if( $question->options->showunits == 3) {
478 $this->valid_numerical_unit == true ;
479 }else {
480 $this->valid_numerical_unit = $this->valid_unit($state->responses[''], $question->options->units);
481 }
482 break;
483 }
484 }
aeb15530
PS
485 // if ($this->valid_numerical_unit) echo "<p>grade responses valid unit </p>";
486 // if (!$this->valid_numerical_unit) echo "<p>grade responses not valid unit </p>";
c5da9906 487 // apply unit penalty
488 $this->raw_unitpenalty = 0 ;
489 if(!empty($question->options->unitpenalty)&& !$this->valid_numerical_unit ){
490 if($question->options->unitgradingtype == 1){
491 $this->raw_unitpenalty = $question->options->unitpenalty* $state->raw_grade ;
492 }else {
493 $this->raw_unitpenalty = $question->options->unitpenalty* $question->maxgrade;
494 }
495 $state->raw_grade -= $question->options->unitpenalty ;
496 }
497 // echo "<p>grade responses <pre>";print_r($state->responses);echo "</pre></p>";
498
499 // $this->raw_unitpenalty = $question->options->unitpenalty ;
500 // Make sure we don't assign negative or too high marks.
501 // $state->raw_grade -= $question->options->unitpenalty ;
502 $state->raw_grade = min(max((float) $state->raw_grade,
503 0.0), 1.0) * $question->maxgrade;
504
505 // Update the penalty.
506 $state->penalty = $question->penalty * $question->maxgrade;
507
508 // mark the state as graded
509 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
510
511 return true;
512 }
aeb15530
PS
513
514
516cf3eb 515 function get_correct_responses(&$question, &$state) {
516 $correct = parent::get_correct_responses($question, $state);
c85aea03 517 $unit = $this->get_default_numerical_unit($question);
78d2d576 518 $correct['answer']= $correct[''];
c85aea03 519 if (isset($correct['']) && $correct[''] != '*' && $unit) {
516cf3eb 520 $correct[''] .= ' '.$unit->unit;
78d2d576 521 $correct['unit']= $unit->unit;
aeb15530 522 }
516cf3eb 523 return $correct;
524 }
525
526 // ULPGC ecastro
527 function get_all_responses(&$question, &$state) {
1fe641f7 528 $result = new stdClass;
529 $answers = array();
516cf3eb 530 $unit = $this->get_default_numerical_unit($question);
531 if (is_array($question->options->answers)) {
532 foreach ($question->options->answers as $aid=>$answer) {
1fe641f7 533 $r = new stdClass;
516cf3eb 534 $r->answer = $answer->answer;
535 $r->credit = $answer->fraction;
536 $this->get_tolerance_interval($answer);
55894a42 537 if ($r->answer != '*' && $unit) {
538 $r->answer .= ' ' . $unit->unit;
516cf3eb 539 }
540 if ($answer->max != $answer->min) {
541 $max = "$answer->max"; //format_float($answer->max, 2);
542 $min = "$answer->min"; //format_float($answer->max, 2);
543 $r->answer .= ' ('.$min.'..'.$max.')';
544 }
545 $answers[$aid] = $r;
546 }
516cf3eb 547 }
548 $result->id = $question->id;
549 $result->responses = $answers;
550 return $result;
551 }
c5da9906 552 function get_actual_response($question, $state) {
553 // echo "<p>state response numerical GET ACTUAL RESPONSE $question->id $question->qtype <pre>";print_r($state);echo "</pre></p>";
aeb15530 554 if (!empty($state->responses) && !empty($state->responses[''])) {
c5da9906 555 if(false === strpos($state->responses[''], '|||||')){
556 $responses[] = $state->responses[''];
557 }else {
558 $resp = explode('|||||', $state->responses['']);
aeb15530 559 $responses[] = $resp[0].$resp[1];
c5da9906 560 }
561 } else {
562 $responses[] = '';
563 }
aeb15530 564
c5da9906 565 return $responses;
566 }
567
516cf3eb 568
569 function get_tolerance_interval(&$answer) {
570 // No tolerance
571 if (empty($answer->tolerance)) {
1fe641f7 572 $answer->tolerance = 0;
516cf3eb 573 }
574
575 // Calculate the interval of correct responses (min/max)
576 if (!isset($answer->tolerancetype)) {
577 $answer->tolerancetype = 2; // nominal
578 }
579
223ad0b9 580 // We need to add a tiny fraction depending on the set precision to make the
516cf3eb 581 // comparison work correctly. Otherwise seemingly equal values can yield
582 // false. (fixes bug #3225)
223ad0b9 583 $tolerance = (float)$answer->tolerance + ("1.0e-".ini_get('precision'));
516cf3eb 584 switch ($answer->tolerancetype) {
585 case '1': case 'relative':
586 /// Recalculate the tolerance and fall through
587 /// to the nominal case:
588 $tolerance = $answer->answer * $tolerance;
dcd4192a 589 // Do not fall through to the nominal case because the tiny fraction is a factor of the answer
590 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
591 $max = $answer->answer + $tolerance;
592 $min = $answer->answer - $tolerance;
593 break;
516cf3eb 594 case '2': case 'nominal':
595 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
dcd4192a 596 // $answer->tolerance 0 or something else
597 if ((float)$answer->tolerance == 0.0 && abs((float)$answer->answer) <= $tolerance ){
f34488b2 598 $tolerance = (float) ("1.0e-".ini_get('precision')) * abs((float)$answer->answer) ; //tiny fraction
dcd4192a 599 } else if ((float)$answer->tolerance != 0.0 && abs((float)$answer->tolerance) < abs((float)$answer->answer) && abs((float)$answer->answer) <= $tolerance){
f34488b2 600 $tolerance = (1+("1.0e-".ini_get('precision')) )* abs((float) $answer->tolerance) ;//tiny fraction
601 }
602
516cf3eb 603 $max = $answer->answer + $tolerance;
604 $min = $answer->answer - $tolerance;
605 break;
ed0ba6da 606 case '3': case 'geometric':
516cf3eb 607 $quotient = 1 + abs($tolerance);
608 $max = $answer->answer * $quotient;
609 $min = $answer->answer / $quotient;
610 break;
611 default:
0b4f4187 612 print_error('unknowntolerance', 'question', '', $answer->tolerancetype);
516cf3eb 613 }
614
615 $answer->min = $min;
616 $answer->max = $max;
617 return true;
618 }
619
620 /**
1fe641f7 621 * Checks if the $rawresponse has a unit and applys it if appropriate.
622 *
623 * @param string $rawresponse The response string to be converted to a float.
624 * @param array $units An array with the defined units, where the
625 * unit is the key and the multiplier the value.
626 * @return float The rawresponse with the unit taken into
627 * account as a float.
628 */
516cf3eb 629 function apply_unit($rawresponse, $units) {
630 // Make units more useful
631 $tmpunits = array();
632 foreach ($units as $unit) {
633 $tmpunits[$unit->unit] = $unit->multiplier;
634 }
1fe641f7 635 // remove spaces and normalise decimal places.
be1bb80e 636 $rawresponse = trim($rawresponse) ;
516cf3eb 637 $search = array(' ', ',');
be1bb80e
PP
638 // test if a . is present or there are multiple , (i.e. 2,456,789 ) so that we don't need spaces and ,
639 if ( strpos($rawresponse,'.' ) !== false || substr_count($rawresponse,',') > 1 ) {
640 $replace = array('', '');
641 }else { // remove spaces and normalise , to a . .
642 $replace = array('', '.');
643 }
644 $rawresponse = str_replace($search, $replace, $rawresponse);
f34488b2 645
1fe641f7 646 // Apply any unit that is present.
6dbcacee 647 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
1fe641f7 648 $rawresponse, $responseparts)) {
f34488b2 649
1fe641f7 650 if (!empty($responseparts[5])) {
f34488b2 651
1fe641f7 652 if (isset($tmpunits[$responseparts[5]])) {
653 // Valid number with unit.
654 return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
c5da9906 655 } else {
aeb15530 656 // Valid number with invalid unit.
c5da9906 657 return (float)$responseparts[1];
658 }
659
660 } else {
661 // Valid number without unit.
662 return (float)$responseparts[1];
663 }
664 }
665 // Invalid number. Must be wrong.
666 return false;
667 }
668 function edit_numerical_options(&$mform, &$that){
669 $mform->addElement('header', 'unithandling', get_string("Units handling", 'qtype_numerical'));
8b831fbb
PP
670 // $mform->addElement('checkbox', 'usecurrentcat1', get_string('Don\'t use unit','qtype_numerical'), get_string('leave Unit(s) input field empty ','qtype_numerical'));
671 $mform->addElement('radio', 'showunits', 'No unit display', get_string("Only numerical answer will be graded: leave Unit No1 empty", 'qtype_numerical'),3);
672 $mform->addElement('radio', 'showunits', 'Display unit ', get_string('NON editable text of Unit No1', 'qtype_numerical'),2);
673 $mform->addElement('radio', 'showunits', 'Edit unit ', get_string('Editable text input element', 'qtype_numerical'),0);
674 $mform->addElement('radio', 'showunits', 'Select units ', get_string('Choice radio element of 2 Units minimum', 'qtype_numerical'),1);
675 $unitslefts = array('0' => get_string('rigth as 1.00cm', 'qtype_numerical'),'1' => get_string('left as $1.00', 'qtype_numerical'));
676 $mform->addElement('select', 'unitsleft', 'Unit position' , $unitslefts );
677 $currentgrp1 = array();
aeb15530 678
c5da9906 679 $currentgrp1[] =& $mform->createElement('text', 'unitpenalty', get_string('Penalty for bad unit', 'qtype_numerical') ,
680 array('size' => 3));
8b831fbb 681 $currentgrp1[] =& $mform->createElement('static', 'penalty1','', get_string('as decimal fraction (0-1) of', 'qtype_numerical'));
c5da9906 682 $mform->addGroup($currentgrp1, 'penaltygrp', get_string('Penalty for bad unit', 'qtype_numerical'), null, false);
683 $mform->setType('unitpenalty', PARAM_NUMBER);
684 //$mform->addRule('unitpenalty', null, 'required', null, 'client');
685 $mform->setDefault('unitpenalty', 0.1);
686 $currentgrp = array();
8b831fbb
PP
687 $unitgradingtypes = array('1' => get_string('question grade', 'qtype_numerical'), '2' => get_string(' response grade', 'qtype_numerical'));
688 $mform->addElement('select', 'unitgradingtype', '' , $unitgradingtypes );
689 /* $currentgrp[] =& $mform->createElement('radio', 'unitgradingtype', 'or', get_string('question grade', 'qtype_numerical'),1);
690 $currentgrp[] =& $mform->createElement('radio', 'unitgradingtype', '', get_string(' response grade', 'qtype_numerical'),2);;*/
c5da9906 691 $mform->setDefault('unitgradingtype', 1);
8b831fbb
PP
692 // $mform->addGroup($currentgrp, 'penaltychoicegrp', '',' or ', false);
693 $mform->setHelpButton('unitgradingtype', array('penaltygrp', get_string('unitpenalty', 'qtype_numerical'), 'qtype_numerical'));
c5da9906 694 $mform->setDefault('showunits', 0);
695 $currentgrp = array();
8b831fbb
PP
696 /* $leftgrp[] =& $mform->createElement('radio', 'unitsleft', '', get_string('left as $1.00', 'qtype_numerical'),1);
697 $leftgrp[] =& $mform->createElement('radio', 'unitsleft', '', get_string('rigth as 1.00cm', 'qtype_numerical'),0);*/
698 // $mform->addGroup($leftgrp, 'unitsleft', 'Unit position',' or ', false);
c5da9906 699 $mform->setDefault('unitsleft', 0);
700 $mform->addElement('htmleditor', 'instructions', get_string('instructions', 'quiz'),
701 array('rows' => 10, 'course' => $that->coursefilesid));
8b831fbb 702 $mform->setAdvanced('instructions',true);
c5da9906 703 $mform->setType('instructions', PARAM_RAW);
704 $mform->setHelpButton('instructions', array('instructions', get_string('instructions', 'quiz'), 'quiz'));
8b831fbb
PP
705 // $mform->disabledIf('showunits', 'usecurrentcat1', 'checked');
706 $mform->disabledIf('addunits', 'showunits','eq','3');
707 $mform->disabledIf('addunits', 'showunits','eq','2');
708 $mform->disabledIf('unitpenalty', 'showunits','eq','3');
709 $mform->disabledIf('unitpenalty', 'showunits','eq','2');
710 $mform->disabledIf('unitgradingtype', 'showunits','eq','3');
711 $mform->disabledIf('unitgradingtype', 'showunits','eq','2');
712 $mform->disabledIf('instructions', 'showunits','eq','3');
713 $mform->disabledIf('instructions', 'showunits','eq','2');
714
715 // $mform->disabledIf('usecurrentcat1', 'showunits','eq','3');
716 // $mform->disabledIf('usecurrentcat1', 'units[0]','eq','3');
717
aeb15530 718
c5da9906 719 }
720
721 function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
722 // echo "<p>state uestion_grading_details $question->id $question->qtype <pre>";print_r($state);echo "</pre></p>";
723
f0b6151c 724 parent::print_question_grading_details($question, $state, $cmoptions, $options);
aeb15530
PS
725
726 }
727
c5da9906 728 function valid_unit($rawresponse, $units) {
729 // Make units more useful
730 $tmpunits = array();
731 foreach ($units as $unit) {
732 $tmpunits[$unit->unit] = $unit->multiplier;
733 }
734 // remove spaces and normalise decimal places.
735 $search = array(' ', ',');
736 $replace = array('', '.');
737 $rawresponse = str_replace($search, $replace, trim($rawresponse));
738
739 // Apply any unit that is present.
740 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
741 $rawresponse, $responseparts)) {
742
743 if (!empty($responseparts[5])) {
744
745 if (isset($tmpunits[$responseparts[5]])) {
746 // Valid number with unit.
747 return true ; //(float)$responseparts[1] / $tmpunits[$responseparts[5]];
1fe641f7 748 } else {
749 // Valid number with invalid unit. Must be wrong.
750 return false;
751 }
752
516cf3eb 753 } else {
1fe641f7 754 // Valid number without unit.
c5da9906 755 return false ; //(float)$responseparts[1];
516cf3eb 756 }
757 }
1fe641f7 758 // Invalid number. Must be wrong.
759 return false;
516cf3eb 760 }
f34488b2 761
1fe641f7 762 /// BACKUP FUNCTIONS ////////////////////////////
c5d94c41 763
1fe641f7 764 /**
c5d94c41 765 * Backup the data in the question
766 *
767 * This is used in question/backuplib.php
768 */
769 function backup($bf,$preferences,$question,$level=6) {
f34488b2 770 global $DB;
c5d94c41 771
772 $status = true;
773
f34488b2 774 $numericals = $DB->get_records('question_numerical', array('question' => $question), 'id ASC');
c5d94c41 775 //If there are numericals
776 if ($numericals) {
777 //Iterate over each numerical
778 foreach ($numericals as $numerical) {
779 $status = fwrite ($bf,start_tag("NUMERICAL",$level,true));
780 //Print numerical contents
781 fwrite ($bf,full_tag("ANSWER",$level+1,false,$numerical->answer));
782 fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$numerical->tolerance));
783 //Now backup numerical_units
784 $status = question_backup_numerical_units($bf,$preferences,$question,7);
785 $status = fwrite ($bf,end_tag("NUMERICAL",$level,true));
786 }
c5da9906 787 $status = question_backup_numerical_options($bf,$preferences,$question,$level);
788 /* $numerical_options = $DB->get_records("question_numerical_options",array("questionid" => $question),"id");
789 if ($numerical_options) {
790 //Iterate over each numerical_option
791 foreach ($numerical_options as $numerical_option) {
792 $status = fwrite ($bf,start_tag("NUMERICAL_OPTIONS",$level,true));
793 //Print numerical_option contents
794 fwrite ($bf,full_tag("INSTRUCTIONS",$level+1,false,$numerical_option->instructions));
795 fwrite ($bf,full_tag("SHOWUNITS",$level+1,false,$numerical_option->showunits));
796 fwrite ($bf,full_tag("UNITSLEFT",$level+1,false,$numerical_option->unitsleft));
797 fwrite ($bf,full_tag("UNITGRADINGTYPE",$level+1,false,$numerical_option->unitgradingtype));
798 fwrite ($bf,full_tag("UNITPENALTY",$level+1,false,$numerical_option->unitpenalty));
799 $status = fwrite ($bf,end_tag("NUMERICAL_OPTIONS",$level,true));
800 }
801 }*/
802
c5d94c41 803 //Now print question_answers
804 $status = question_backup_answers($bf,$preferences,$question);
805 }
806 return $status;
807 }
808
1fe641f7 809 /// RESTORE FUNCTIONS /////////////////
315559d3 810
1fe641f7 811 /**
315559d3 812 * Restores the data in the question
813 *
814 * This is used in question/restorelib.php
815 */
816 function restore($old_question_id,$new_question_id,$info,$restore) {
9db7dab2 817 global $DB;
315559d3 818
819 $status = true;
820
821 //Get the numerical array
27cabbe6 822 if (isset($info['#']['NUMERICAL'])) {
823 $numericals = $info['#']['NUMERICAL'];
824 } else {
825 $numericals = array();
826 }
315559d3 827
828 //Iterate over numericals
829 for($i = 0; $i < sizeof($numericals); $i++) {
830 $num_info = $numericals[$i];
831
832 //Now, build the question_numerical record structure
1fe641f7 833 $numerical = new stdClass;
315559d3 834 $numerical->question = $new_question_id;
835 $numerical->answer = backup_todb($num_info['#']['ANSWER']['0']['#']);
836 $numerical->tolerance = backup_todb($num_info['#']['TOLERANCE']['0']['#']);
837
55894a42 838 //We have to recode the answer field
315559d3 839 $answer = backup_getid($restore->backup_unique_code,"question_answers",$numerical->answer);
840 if ($answer) {
841 $numerical->answer = $answer->new_id;
842 }
843
844 //The structure is equal to the db, so insert the question_numerical
9db7dab2 845 $newid = $DB->insert_record ("question_numerical", $numerical);
315559d3 846
847 //Do some output
848 if (($i+1) % 50 == 0) {
849 if (!defined('RESTORE_SILENTLY')) {
850 echo ".";
851 if (($i+1) % 1000 == 0) {
852 echo "<br />";
853 }
854 }
855 backup_flush(300);
856 }
857
858 //Now restore numerical_units
859 $status = question_restore_numerical_units ($old_question_id,$new_question_id,$num_info,$restore);
860
c5da9906 861 //Now restore numerical_options
862 $status = question_restore_numerical_options ($old_question_id,$new_question_id,$num_info,$restore);
863
315559d3 864 if (!$newid) {
865 $status = false;
866 }
867 }
868
869 return $status;
870 }
871
b9bd6da4 872 /**
873 * Runs all the code required to set up and save an essay question for testing purposes.
874 * Alternate DB table prefix may be used to facilitate data deletion.
875 */
876 function generate_test($name, $courseid = null) {
877 global $DB;
878 list($form, $question) = default_questiontype::generate_test($name, $courseid);
879 $question->category = $form->category;
880
881 $form->questiontext = "What is 674 * 36?";
882 $form->generalfeedback = "Thank you";
883 $form->penalty = 0.1;
884 $form->defaultgrade = 1;
885 $form->noanswers = 3;
886 $form->answer = array('24264', '24264', '1');
887 $form->tolerance = array(10, 100, 0);
888 $form->fraction = array(1, 0.5, 0);
889 $form->nounits = 2;
890 $form->unit = array(0 => null, 1 => null);
891 $form->multiplier = array(1, 0);
892 $form->feedback = array('Very good', 'Close, but not quite there', 'Well at least you tried....');
893
894 if ($courseid) {
895 $course = $DB->get_record('course', array('id' => $courseid));
896 }
897
898 return $this->save_question($question, $form, $course);
899 }
516cf3eb 900}
516cf3eb 901
1fe641f7 902// INITIATION - Without this line the question type is not in use.
a2156789 903question_register_questiontype(new question_numerical_qtype());
aeb15530 904