MDL-20601 fixed fatal SQL upgrade error, missing {} around table names
[moodle.git] / question / type / numerical / questiontype.php
CommitLineData
4d41f4ee 1<?php // $Id$
1fe641f7 2/**
3 * @version $Id$
4 * @author Martin Dougiamas and many others. Tim Hunt.
5 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
1976496e 6 * @package questionbank
7 * @subpackage questiontypes
1fe641f7 8 *//** */
516cf3eb 9
aaae75b0 10require_once("$CFG->dirroot/question/type/shortanswer/questiontype.php");
516cf3eb 11
1fe641f7 12/**
13 * NUMERICAL QUESTION TYPE CLASS
14 *
15 * This class contains some special features in order to make the
16 * question type embeddable within a multianswer (cloze) question
17 *
18 * This question type behaves like shortanswer in most cases.
19 * Therefore, it extends the shortanswer question type...
1976496e 20 * @package questionbank
21 * @subpackage questiontypes
1fe641f7 22 */
32a189d6 23class question_numerical_qtype extends question_shortanswer_qtype {
516cf3eb 24
c5da9906 25 public $virtualqtype = false;
26 public $unitpenalty = 0;
27 public $raw_unitgrade = 0 ;
28 public $raw_unitpenalty = 0 ;
29 public $valid_numerical_unit = true ;
516cf3eb 30 function name() {
31 return 'numerical';
32 }
869309b8 33
34 function has_wildcards_in_responses() {
35 return true;
36 }
37
38 function requires_qtypes() {
39 return array('shortanswer');
40 }
516cf3eb 41
42 function get_question_options(&$question) {
43 // Get the question answers and their respective tolerances
32a189d6 44 // Note: question_numerical is an extension of the answer table rather than
4f48fb42 45 // the question table as is usually the case for qtype
516cf3eb 46 // specific tables.
fef8f84e 47 global $CFG, $DB, $OUTPUT;
c5da9906 48 $this->get_numerical_options($question);
f34488b2 49 if (!$question->options->answers = $DB->get_records_sql(
516cf3eb 50 "SELECT a.*, n.tolerance " .
f34488b2 51 "FROM {question_answers} a, " .
52 " {question_numerical} n " .
53 "WHERE a.question = ? " .
026bec73 54 " AND a.id = n.answer " .
f34488b2 55 "ORDER BY a.id ASC", array($question->id))) {
fef8f84e 56 echo $OUTPUT->notification('Error: Missing question answer for numerical question ' . $question->id . '!');
516cf3eb 57 return false;
58 }
59 $this->get_numerical_units($question);
60
5a14d563 61 // If units are defined we strip off the default unit from the answer, if
516cf3eb 62 // it is present. (Required for compatibility with the old code and DB).
63 if ($defaultunit = $this->get_default_numerical_unit($question)) {
64 foreach($question->options->answers as $key => $val) {
65 $answer = trim($val->answer);
66 $length = strlen($defaultunit->unit);
1fe641f7 67 if ($length && substr($answer, -$length) == $defaultunit->unit) {
516cf3eb 68 $question->options->answers[$key]->answer =
1fe641f7 69 substr($answer, 0, strlen($answer)-$length);
516cf3eb 70 }
71 }
72 }
c5da9906 73
74 return true;
75 }
76 function get_numerical_options(&$question) {
77 global $DB;
78 if (!$options = $DB->get_record('question_numerical_options', array('question' => $question->id))) {
79 // echo $OUTPUT->notification('Error: Missing question options for numerical question'.$question->id.'!');
80 // return false;
81 $question->options->unitgradingtype = 0;
82 $question->options->unitpenalty = 0;
83 $question->options->showunits = 0 ;
84 $question->options->unitsleft = 0 ;
85 $question->options->instructions = '' ;
86 } else {
87 $question->options->unitgradingtype = $options->unitgradingtype;
88 $question->options->unitpenalty = $options->unitpenalty;
89 $question->options->showunits = $options->showunits ;
90 $question->options->unitsleft = $options->unitsleft ;
91 $question->options->instructions = $options->instructions ;
92 }
93
94
516cf3eb 95 return true;
96 }
516cf3eb 97 function get_numerical_units(&$question) {
f34488b2 98 global $DB;
99 if ($units = $DB->get_records('question_numerical_units', array('question' => $question->id), 'id ASC')) {
3a513ba4 100 $units = array_values($units);
516cf3eb 101 } else {
b4d7d27c 102 $units = array();
516cf3eb 103 }
b4d7d27c 104 foreach ($units as $key => $unit) {
105 $units[$key]->multiplier = clean_param($unit->multiplier, PARAM_NUMBER);
106 }
107 $question->options->units = $units;
516cf3eb 108 return true;
109 }
110
111 function get_default_numerical_unit(&$question) {
ed0ba6da 112 if (isset($question->options->units[0])) {
113 foreach ($question->options->units as $unit) {
114 if (abs($unit->multiplier - 1.0) < '1.0e-' . ini_get('precision')) {
115 return $unit;
516cf3eb 116 }
117 }
118 }
ed0ba6da 119 return false;
516cf3eb 120 }
121
1fe641f7 122 /**
123 * Save the units and the answers associated with this question.
124 */
516cf3eb 125 function save_question_options($question) {
f34488b2 126 global $DB;
516cf3eb 127 // Get old versions of the objects
f34488b2 128 if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) {
516cf3eb 129 $oldanswers = array();
130 }
131
f34488b2 132 if (!$oldoptions = $DB->get_records('question_numerical', array('question' => $question->id), 'answer ASC')) {
516cf3eb 133 $oldoptions = array();
134 }
135
1fe641f7 136 // Save the units.
516cf3eb 137 $result = $this->save_numerical_units($question);
138 if (isset($result->error)) {
139 return $result;
140 } else {
141 $units = &$result->units;
142 }
143
144 // Insert all the new answers
145 foreach ($question->answer as $key => $dataanswer) {
94a6d656 146 // Check for, and ingore, completely blank answer from the form.
147 if (trim($dataanswer) == '' && $question->fraction[$key] == 0 &&
148 html_is_blank($question->feedback[$key])) {
149 continue;
150 }
516cf3eb 151
94a6d656 152 $answer = new stdClass;
153 $answer->question = $question->id;
fac1189d 154 if (trim($dataanswer) === '*') {
94a6d656 155 $answer->answer = '*';
156 } else {
157 $answer->answer = $this->apply_unit($dataanswer, $units);
158 if ($answer->answer === false) {
159 $result->notice = get_string('invalidnumericanswer', 'quiz');
516cf3eb 160 }
94a6d656 161 }
162 $answer->fraction = $question->fraction[$key];
163 $answer->feedback = trim($question->feedback[$key]);
516cf3eb 164
94a6d656 165 if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it
166 $answer->id = $oldanswer->id;
0bcf8b6f 167 $DB->update_record("question_answers", $answer);
94a6d656 168 } else { // This is a completely new answer
0bcf8b6f 169 $answer->id = $DB->insert_record("question_answers", $answer);
94a6d656 170 }
f34488b2 171
94a6d656 172 // Set up the options object
173 if (!$options = array_shift($oldoptions)) {
174 $options = new stdClass;
175 }
176 $options->question = $question->id;
177 $options->answer = $answer->id;
178 if (trim($question->tolerance[$key]) == '') {
179 $options->tolerance = '';
180 } else {
181 $options->tolerance = $this->apply_unit($question->tolerance[$key], $units);
182 if ($options->tolerance === false) {
183 $result->notice = get_string('invalidnumerictolerance', 'quiz');
184 }
185 }
186
187 // Save options
188 if (isset($options->id)) { // reusing existing record
0bcf8b6f 189 $DB->update_record('question_numerical', $options);
94a6d656 190 } else { // new options
0bcf8b6f 191 $DB->insert_record('question_numerical', $options);
1fe641f7 192 }
193 }
194 // delete old answer records
195 if (!empty($oldanswers)) {
196 foreach($oldanswers as $oa) {
f34488b2 197 $DB->delete_records('question_answers', array('id' => $oa->id));
1fe641f7 198 }
199 }
516cf3eb 200
1fe641f7 201 // delete old answer records
202 if (!empty($oldoptions)) {
203 foreach($oldoptions as $oo) {
f34488b2 204 $DB->delete_records('question_numerical', array('id' => $oo->id));
516cf3eb 205 }
206 }
c5da9906 207 $result = $this->save_numerical_options($question);
208 if (isset($result->error)) {
209 return $result;
210 }
1fe641f7 211 // Report any problems.
212 if (!empty($result->notice)) {
213 return $result;
214 }
1fe641f7 215 return true;
516cf3eb 216 }
217
c5da9906 218 function save_numerical_options(&$question) {
219 global $DB;
220 $result = new stdClass;
221 // numerical options
222 $update = true ;
223 $options = $DB->get_record("question_numerical_options", array("question" => $question->id));
224 if (!$options) {
225 $update = false;
226 $options = new stdClass;
227 $options->question = $question->id;
228 }
229 if(isset($question->unitgradingtype)){
230 $options->unitgradingtype = $question->unitgradingtype;
231 }else {
232 $options->unitgradingtype = 0 ;
233 }
234 if(isset($question->unitpenalty)){
235 $options->unitpenalty = $question->unitpenalty;
236 }else {
237 $options->unitpenalty = 0 ;
238 }
239 if(isset($question->showunits)){
240 $options->showunits = $question->showunits;
241 }else {
242 $options->showunits = 0 ;
243 }
244 if(isset($question->unitsleft)){
245 $options->unitsleft = $question->unitsleft;
246 }else {
247 $options->unitsleft = 0 ;
248 }
249 if(isset($question->instructions)){
250 $options->instructions = trim($question->instructions);
251 }else {
252 $options->instructions = '' ;
253 }
254 if ($update) {
255 if (!$DB->update_record("question_numerical_options", $options)) {
256 $result->error = "Could not update numerical question options! (id=$options->id)";
257 return $result;
258 }
259 } else {
260 if (!$DB->insert_record("question_numerical_options", $options)) {
261 $result->error = "Could not insert numerical question options!";
262 return $result;
263 }
264 }
265 return $result;
266 }
267
516cf3eb 268 function save_numerical_units($question) {
f34488b2 269 global $DB;
ed0ba6da 270 $result = new stdClass;
516cf3eb 271
ed0ba6da 272 // Delete the units previously saved for this question.
f34488b2 273 $DB->delete_records('question_numerical_units', array('question' => $question->id));
ed0ba6da 274
26b26662 275 // Nothing to do.
276 if (!isset($question->multiplier)) {
277 $result->units = array();
278 return $result;
279 }
280
ed0ba6da 281 // Save the new units.
516cf3eb 282 $units = array();
ed0ba6da 283 foreach ($question->multiplier as $i => $multiplier) {
516cf3eb 284 // Discard any unit which doesn't specify the unit or the multiplier
285 if (!empty($question->multiplier[$i]) && !empty($question->unit[$i])) {
ed0ba6da 286 $units[$i] = new stdClass;
516cf3eb 287 $units[$i]->question = $question->id;
1fe641f7 288 $units[$i]->multiplier = $this->apply_unit($question->multiplier[$i], array());
516cf3eb 289 $units[$i]->unit = $question->unit[$i];
979425b5 290 $DB->insert_record('question_numerical_units', $units[$i]);
516cf3eb 291 }
292 }
293 unset($question->multiplier, $question->unit);
294
516cf3eb 295 $result->units = &$units;
296 return $result;
297 }
298
c5da9906 299 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
300 $state->responses = array();
301 $state->responses['answer'] = '';
302 $state->responses['unit'] = '';
303 /* if ($question->options->showunits == 1){
304 $state->responses['unit'] = '0';
305 }*/
306
307 return true;
308 }
309 function restore_session_and_responses(&$question, &$state) {
310 if(false === strpos($state->responses[''], '|||||')){
311 // temporary
312 $state->responses['answer']= $state->responses[''];
313 $state->responses['unit'] = '';
314 split_old_answer($state->responses[''], $question->options->units, $state->responses['answer'] ,$state->responses['unit'] );
315 }else {
316 $responses = explode('|||||', $state->responses['']);
317 $state->responses['answer']= $responses[0];
318 $state->responses['unit'] = $responses[1];
319 }
320 // echo "<p> restore response $responses || <pre>";print_r($state);echo "</pre></p>";
321
322 /*
323 if ($question->options->showunits == 1 && isset($question->options->units)){
324 $state->responses['unit']=$this->find_unit_index($question,$state->responses['unit']);
325 }*/
326 return true;
327 }
328
329 function find_unit_index(&$question,$value){
330 $length = 0;
331 $goodkey = 0 ;
332 foreach ($question->options->units as $key => $unit){
333 if($unit->unit ==$value ) {
334 return $key ;
335 }
336 }
337 return 0 ;
338 }
339
340 function split_old_answer($rawresponse, $units, &$answer ,&$unit ) {
341 $answer = $rawresponse ;
342 // remove spaces and normalise decimal places.
343 $search = array(' ', ',');
344 $replace = array('', '.');
345 $rawresponse = str_replace($search, $replace, trim($rawresponse));
346 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
347 $rawresponse, $responseparts)) {
348 $unit = $responseparts[5] ;
349 $answer = $responseparts[1] ;
350 }
351 return ;
352 }
353
354
355 function save_session_and_responses(&$question, &$state) {
356 global $DB;
357 // echo "<p> save session <pre>";print_r($state);echo "</pre></p>";
358
359 $responses = '';
360 if(isset($state->responses['unit']) && isset($question->options->units[$state->responses['unit']])){
361 $responses = $state->responses['answer'].'|||||'.$question->options->units[$state->responses['unit']]->unit;
362 }else if(isset($state->responses['unit'])){
363 $responses = $state->responses['answer'].'|||||'.$state->responses['unit'] ;
364 }else {
365 $responses = $state->responses['answer'].'|||||';
366 }
367 // Set the legacy answer field
368 if (!$DB->set_field('question_states', 'answer', $responses, array('id' => $state->id))) {
369 return false;
370 }
371 return true;
372 }
373
374/**
1fe641f7 375 * Deletes question from the question-type specific tables
376 *
377 * @return boolean Success/Failure
378 * @param object $question The question being deleted
379 */
90c3f310 380 function delete_question($questionid) {
f34488b2 381 global $DB;
382 $DB->delete_records("question_numerical", array("question" => $questionid));
c5da9906 383 $DB->delete_records("question_numerical_options", array("question" => $questionid));
f34488b2 384 $DB->delete_records("question_numerical_units", array("question" => $questionid));
516cf3eb 385 return true;
386 }
c5da9906 387
516cf3eb 388
5a14d563 389 function compare_responses(&$question, $state, $teststate) {
c5da9906 390 if ($question->options->showunits == 1 && isset($question->options->units) && isset($question->options->units[$state->responses['unit']] )){
391 $state->responses['unit']=$question->options->units[$state->responses['unit']]->unit;
392 };
393
394
395 $responses = '';
396 $testresponses = '';
397 if (isset($state->responses['answer'])){
398 $responses = $state->responses['answer'];
399 }
400 if (isset($state->responses['unit'])){
401 $responses .= $state->responses['unit'];
402 }
403 if (isset($teststate->responses['answer'])){
404 $testresponses = $teststate->responses['answer'];
405 }
406 if (isset($teststate->responses['unit'])){
407 $testresponses .= $teststate->responses['unit'];
408 }
c5da9906 409 // echo "<p> compare response $responses || $testresponses <pre>";print_r($state);echo "</pre></p>";
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>";
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);
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>";
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>";
461
462 //first the split response from unit choice display is converted as
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 }
467 */
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
470 // however it is used to detect good or bad numbers but also
471 // gives false
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 }
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>";
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 }
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;
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>";
554 if (!empty($state->responses) && !empty($state->responses[''])) {
555 if(false === strpos($state->responses[''], '|||||')){
556 $responses[] = $state->responses[''];
557 }else {
558 $resp = explode('|||||', $state->responses['']);
559 $responses[] = $resp[0].$resp[1];
560 }
561 } else {
562 $responses[] = '';
563 }
564
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.
516cf3eb 636 $search = array(' ', ',');
637 $replace = array('', '.');
1fe641f7 638 $rawresponse = str_replace($search, $replace, trim($rawresponse));
f34488b2 639
1fe641f7 640 // Apply any unit that is present.
6dbcacee 641 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
1fe641f7 642 $rawresponse, $responseparts)) {
f34488b2 643
1fe641f7 644 if (!empty($responseparts[5])) {
f34488b2 645
1fe641f7 646 if (isset($tmpunits[$responseparts[5]])) {
647 // Valid number with unit.
648 return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
c5da9906 649 } else {
650 // Valid number with invalid unit.
651 return (float)$responseparts[1];
652 }
653
654 } else {
655 // Valid number without unit.
656 return (float)$responseparts[1];
657 }
658 }
659 // Invalid number. Must be wrong.
660 return false;
661 }
662 function edit_numerical_options(&$mform, &$that){
663 $mform->addElement('header', 'unithandling', get_string("Units handling", 'qtype_numerical'));
664 $currentgrp1 = array();
665
666 $currentgrp1[] =& $mform->createElement('text', 'unitpenalty', get_string('Penalty for bad unit', 'qtype_numerical') ,
667 array('size' => 3));
668 $currentgrp1[] =& $mform->createElement('static', 'penalty1','hello', get_string('as decimal fraction (0-1) of', 'qtype_numerical'));
669 $mform->addGroup($currentgrp1, 'penaltygrp', get_string('Penalty for bad unit', 'qtype_numerical'), null, false);
670 $mform->setType('unitpenalty', PARAM_NUMBER);
671 //$mform->addRule('unitpenalty', null, 'required', null, 'client');
672 $mform->setDefault('unitpenalty', 0.1);
673 $currentgrp = array();
674 $currentgrp[] =& $mform->createElement('radio', 'unitgradingtype', 'or', get_string('question grade', 'qtype_numerical'),1);
675 $currentgrp[] =& $mform->createElement('radio', 'unitgradingtype', '', get_string(' response grade', 'qtype_numerical'),2);
676 $mform->setDefault('unitgradingtype', 1);
677 $mform->addGroup($currentgrp, 'penaltychoicegrp', '',' or ', false);
678 $mform->setHelpButton('penaltychoicegrp', array('penaltygrp', get_string('unitpenalty', 'qtype_numerical'), 'qtype_numerical'));
679 $mform->addElement('radio', 'showunits', 'Edit unit ', get_string('Editable text input element', 'qtype_numerical'),0);
680 $mform->addElement('radio', 'showunits', 'Select units ', get_string('Choice radio element', 'qtype_numerical'),1);
681 $mform->addElement('radio', 'showunits', 'Display unit ', get_string('NON editable text of Unit No1', 'qtype_numerical'),2);
682 $mform->addElement('radio', 'showunits', 'No unit display', get_string("Only numerical answer will be graded leave Unit No1 empty", 'qtype_numerical'),3);
683 $mform->setDefault('showunits', 0);
684 $currentgrp = array();
685 $leftgrp[] =& $mform->createElement('radio', 'unitsleft', '', get_string('left as $1.00', 'qtype_numerical'),1);
686 $leftgrp[] =& $mform->createElement('radio', 'unitsleft', '', get_string('rigth as 1.00cm', 'qtype_numerical'),0);
687 $mform->addGroup($leftgrp, 'unitsleft', 'Unit position',' or ', false);
688 $mform->setDefault('unitsleft', 0);
689 $mform->addElement('htmleditor', 'instructions', get_string('instructions', 'quiz'),
690 array('rows' => 10, 'course' => $that->coursefilesid));
691 $mform->setType('instructions', PARAM_RAW);
692 $mform->setHelpButton('instructions', array('instructions', get_string('instructions', 'quiz'), 'quiz'));
693
694
695 }
696
697 function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
698 // echo "<p>state uestion_grading_details $question->id $question->qtype <pre>";print_r($state);echo "</pre></p>";
699
f0b6151c 700 parent::print_question_grading_details($question, $state, $cmoptions, $options);
c5da9906 701
702 }
703
704 function valid_unit($rawresponse, $units) {
705 // Make units more useful
706 $tmpunits = array();
707 foreach ($units as $unit) {
708 $tmpunits[$unit->unit] = $unit->multiplier;
709 }
710 // remove spaces and normalise decimal places.
711 $search = array(' ', ',');
712 $replace = array('', '.');
713 $rawresponse = str_replace($search, $replace, trim($rawresponse));
714
715 // Apply any unit that is present.
716 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
717 $rawresponse, $responseparts)) {
718
719 if (!empty($responseparts[5])) {
720
721 if (isset($tmpunits[$responseparts[5]])) {
722 // Valid number with unit.
723 return true ; //(float)$responseparts[1] / $tmpunits[$responseparts[5]];
1fe641f7 724 } else {
725 // Valid number with invalid unit. Must be wrong.
726 return false;
727 }
728
516cf3eb 729 } else {
1fe641f7 730 // Valid number without unit.
c5da9906 731 return false ; //(float)$responseparts[1];
516cf3eb 732 }
733 }
1fe641f7 734 // Invalid number. Must be wrong.
735 return false;
516cf3eb 736 }
f34488b2 737
1fe641f7 738 /// BACKUP FUNCTIONS ////////////////////////////
c5d94c41 739
1fe641f7 740 /**
c5d94c41 741 * Backup the data in the question
742 *
743 * This is used in question/backuplib.php
744 */
745 function backup($bf,$preferences,$question,$level=6) {
f34488b2 746 global $DB;
c5d94c41 747
748 $status = true;
749
f34488b2 750 $numericals = $DB->get_records('question_numerical', array('question' => $question), 'id ASC');
c5d94c41 751 //If there are numericals
752 if ($numericals) {
753 //Iterate over each numerical
754 foreach ($numericals as $numerical) {
755 $status = fwrite ($bf,start_tag("NUMERICAL",$level,true));
756 //Print numerical contents
757 fwrite ($bf,full_tag("ANSWER",$level+1,false,$numerical->answer));
758 fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$numerical->tolerance));
759 //Now backup numerical_units
760 $status = question_backup_numerical_units($bf,$preferences,$question,7);
761 $status = fwrite ($bf,end_tag("NUMERICAL",$level,true));
762 }
c5da9906 763 $status = question_backup_numerical_options($bf,$preferences,$question,$level);
764 /* $numerical_options = $DB->get_records("question_numerical_options",array("questionid" => $question),"id");
765 if ($numerical_options) {
766 //Iterate over each numerical_option
767 foreach ($numerical_options as $numerical_option) {
768 $status = fwrite ($bf,start_tag("NUMERICAL_OPTIONS",$level,true));
769 //Print numerical_option contents
770 fwrite ($bf,full_tag("INSTRUCTIONS",$level+1,false,$numerical_option->instructions));
771 fwrite ($bf,full_tag("SHOWUNITS",$level+1,false,$numerical_option->showunits));
772 fwrite ($bf,full_tag("UNITSLEFT",$level+1,false,$numerical_option->unitsleft));
773 fwrite ($bf,full_tag("UNITGRADINGTYPE",$level+1,false,$numerical_option->unitgradingtype));
774 fwrite ($bf,full_tag("UNITPENALTY",$level+1,false,$numerical_option->unitpenalty));
775 $status = fwrite ($bf,end_tag("NUMERICAL_OPTIONS",$level,true));
776 }
777 }*/
778
c5d94c41 779 //Now print question_answers
780 $status = question_backup_answers($bf,$preferences,$question);
781 }
782 return $status;
783 }
784
1fe641f7 785 /// RESTORE FUNCTIONS /////////////////
315559d3 786
1fe641f7 787 /**
315559d3 788 * Restores the data in the question
789 *
790 * This is used in question/restorelib.php
791 */
792 function restore($old_question_id,$new_question_id,$info,$restore) {
9db7dab2 793 global $DB;
315559d3 794
795 $status = true;
796
797 //Get the numerical array
27cabbe6 798 if (isset($info['#']['NUMERICAL'])) {
799 $numericals = $info['#']['NUMERICAL'];
800 } else {
801 $numericals = array();
802 }
315559d3 803
804 //Iterate over numericals
805 for($i = 0; $i < sizeof($numericals); $i++) {
806 $num_info = $numericals[$i];
807
808 //Now, build the question_numerical record structure
1fe641f7 809 $numerical = new stdClass;
315559d3 810 $numerical->question = $new_question_id;
811 $numerical->answer = backup_todb($num_info['#']['ANSWER']['0']['#']);
812 $numerical->tolerance = backup_todb($num_info['#']['TOLERANCE']['0']['#']);
813
55894a42 814 //We have to recode the answer field
315559d3 815 $answer = backup_getid($restore->backup_unique_code,"question_answers",$numerical->answer);
816 if ($answer) {
817 $numerical->answer = $answer->new_id;
818 }
819
820 //The structure is equal to the db, so insert the question_numerical
9db7dab2 821 $newid = $DB->insert_record ("question_numerical", $numerical);
315559d3 822
823 //Do some output
824 if (($i+1) % 50 == 0) {
825 if (!defined('RESTORE_SILENTLY')) {
826 echo ".";
827 if (($i+1) % 1000 == 0) {
828 echo "<br />";
829 }
830 }
831 backup_flush(300);
832 }
833
834 //Now restore numerical_units
835 $status = question_restore_numerical_units ($old_question_id,$new_question_id,$num_info,$restore);
836
c5da9906 837 //Now restore numerical_options
838 $status = question_restore_numerical_options ($old_question_id,$new_question_id,$num_info,$restore);
839
315559d3 840 if (!$newid) {
841 $status = false;
842 }
843 }
844
845 return $status;
846 }
847
b9bd6da4 848 /**
849 * Runs all the code required to set up and save an essay question for testing purposes.
850 * Alternate DB table prefix may be used to facilitate data deletion.
851 */
852 function generate_test($name, $courseid = null) {
853 global $DB;
854 list($form, $question) = default_questiontype::generate_test($name, $courseid);
855 $question->category = $form->category;
856
857 $form->questiontext = "What is 674 * 36?";
858 $form->generalfeedback = "Thank you";
859 $form->penalty = 0.1;
860 $form->defaultgrade = 1;
861 $form->noanswers = 3;
862 $form->answer = array('24264', '24264', '1');
863 $form->tolerance = array(10, 100, 0);
864 $form->fraction = array(1, 0.5, 0);
865 $form->nounits = 2;
866 $form->unit = array(0 => null, 1 => null);
867 $form->multiplier = array(1, 0);
868 $form->feedback = array('Very good', 'Close, but not quite there', 'Well at least you tried....');
869
870 if ($courseid) {
871 $course = $DB->get_record('course', array('id' => $courseid));
872 }
873
874 return $this->save_question($question, $form, $course);
875 }
516cf3eb 876}
516cf3eb 877
1fe641f7 878// INITIATION - Without this line the question type is not in use.
a2156789 879question_register_questiontype(new question_numerical_qtype());
516cf3eb 880?>