MDL-25104 removing unnecessary $options = array_shift($oldoptions) line 192 that...
[moodle.git] / question / type / numerical / questiontype.php
CommitLineData
aeb15530 1<?php
fe6ce234
DC
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
1fe641f7 18/**
1fe641f7 19 * @author Martin Dougiamas and many others. Tim Hunt.
20 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
1976496e 21 * @package questionbank
22 * @subpackage questiontypes
fe6ce234 23 */
516cf3eb 24
aaae75b0 25require_once("$CFG->dirroot/question/type/shortanswer/questiontype.php");
516cf3eb 26
1fe641f7 27/**
28 * NUMERICAL QUESTION TYPE CLASS
29 *
30 * This class contains some special features in order to make the
31 * question type embeddable within a multianswer (cloze) question
32 *
33 * This question type behaves like shortanswer in most cases.
34 * Therefore, it extends the shortanswer question type...
1976496e 35 * @package questionbank
36 * @subpackage questiontypes
1fe641f7 37 */
fe6ce234 38
32a189d6 39class question_numerical_qtype extends question_shortanswer_qtype {
516cf3eb 40
c5da9906 41 public $virtualqtype = false;
516cf3eb 42 function name() {
43 return 'numerical';
44 }
aeb15530 45
869309b8 46 function has_wildcards_in_responses() {
47 return true;
48 }
49
50 function requires_qtypes() {
51 return array('shortanswer');
52 }
516cf3eb 53
54 function get_question_options(&$question) {
55 // Get the question answers and their respective tolerances
32a189d6 56 // Note: question_numerical is an extension of the answer table rather than
4f48fb42 57 // the question table as is usually the case for qtype
516cf3eb 58 // specific tables.
fef8f84e 59 global $CFG, $DB, $OUTPUT;
f34488b2 60 if (!$question->options->answers = $DB->get_records_sql(
516cf3eb 61 "SELECT a.*, n.tolerance " .
f34488b2 62 "FROM {question_answers} a, " .
63 " {question_numerical} n " .
64 "WHERE a.question = ? " .
026bec73 65 " AND a.id = n.answer " .
f34488b2 66 "ORDER BY a.id ASC", array($question->id))) {
fef8f84e 67 echo $OUTPUT->notification('Error: Missing question answer for numerical question ' . $question->id . '!');
516cf3eb 68 return false;
69 }
70 $this->get_numerical_units($question);
cf146692 71 //get_numerical_options() need to know if there are units
fe6ce234 72 // to set correctly default values
cf146692 73 $this->get_numerical_options($question);
516cf3eb 74
5a14d563 75 // If units are defined we strip off the default unit from the answer, if
516cf3eb 76 // it is present. (Required for compatibility with the old code and DB).
77 if ($defaultunit = $this->get_default_numerical_unit($question)) {
78 foreach($question->options->answers as $key => $val) {
79 $answer = trim($val->answer);
80 $length = strlen($defaultunit->unit);
1fe641f7 81 if ($length && substr($answer, -$length) == $defaultunit->unit) {
516cf3eb 82 $question->options->answers[$key]->answer =
1fe641f7 83 substr($answer, 0, strlen($answer)-$length);
516cf3eb 84 }
85 }
86 }
c5da9906 87
88 return true;
89 }
92b36005
PP
90 function get_numerical_units(&$question) {
91 global $DB;
92 if ($units = $DB->get_records('question_numerical_units', array('question' => $question->id), 'id ASC')) {
93 $units = array_values($units);
94 } else {
95 $units = array();
96 }
97 foreach ($units as $key => $unit) {
98 $units[$key]->multiplier = clean_param($unit->multiplier, PARAM_NUMBER);
99 }
100 $question->options->units = $units;
101 return true;
102 }
103
104 function get_default_numerical_unit(&$question) {
105 if (isset($question->options->units[0])) {
106 foreach ($question->options->units as $unit) {
107 if (abs($unit->multiplier - 1.0) < '1.0e-' . ini_get('precision')) {
108 return $unit;
109 }
110 }
111 }
112 return false;
113 }
114
c5da9906 115 function get_numerical_options(&$question) {
116 global $DB;
cf146692 117 if (!$options = $DB->get_record('question_numerical_options', array('question' => $question->id))) {
f1ee48c8 118 $question->options->unitgradingtype = 0; // total grade
92b36005 119 $question->options->unitpenalty = 0.1; // default for old questions
fe6ce234 120 // the default
cf146692
PP
121 if ($defaultunit = $this->get_default_numerical_unit($question)) {
122 // so units can be graded
04e91671 123 $question->options->showunits = NUMERICALQUESTIONUNITTEXTINPUTDISPLAY ;
cf146692
PP
124 }else {
125 // only numerical will be graded
04e91671 126 $question->options->showunits = NUMERICALQUESTIONUNITNODISPLAY ;
cf146692
PP
127 }
128 $question->options->unitsleft = 0 ;
fe6ce234
DC
129 $question->options->instructions = '';
130 $question->options->instructionsformat = editors_get_preferred_format();
c5da9906 131 } else {
cf146692
PP
132 $question->options->unitgradingtype = $options->unitgradingtype;
133 $question->options->unitpenalty = $options->unitpenalty;
fe6ce234
DC
134 $question->options->showunits = $options->showunits;
135 $question->options->unitsleft = $options->unitsleft;
136 $question->options->instructions = $options->instructions;
137 $question->options->instructionsformat = $options->instructionsformat;
c5da9906 138 }
aeb15530 139
516cf3eb 140 return true;
141 }
fe6ce234 142
516cf3eb 143
1fe641f7 144 /**
145 * Save the units and the answers associated with this question.
146 */
516cf3eb 147 function save_question_options($question) {
f34488b2 148 global $DB;
fe6ce234
DC
149 $context = $question->context;
150
516cf3eb 151 // Get old versions of the objects
69988ed4
TH
152 $oldanswers = $DB->get_records('question_answers',
153 array('question' => $question->id), 'id ASC');
154 $oldoptions = $DB->get_records('question_numerical',
155 array('question' => $question->id), 'answer ASC');
516cf3eb 156
1fe641f7 157 // Save the units.
516cf3eb 158 $result = $this->save_numerical_units($question);
159 if (isset($result->error)) {
160 return $result;
161 } else {
69988ed4 162 $units = $result->units;
516cf3eb 163 }
164
165 // Insert all the new answers
69988ed4 166 foreach ($question->answer as $key => $answerdata) {
94a6d656 167 // Check for, and ingore, completely blank answer from the form.
69988ed4 168 if (trim($answerdata) == '' && $question->fraction[$key] == 0 &&
fe6ce234 169 html_is_blank($question->feedback[$key]['text'])) {
94a6d656 170 continue;
171 }
516cf3eb 172
69988ed4
TH
173 // Update an existing answer if possible.
174 $answer = array_shift($oldanswers);
175 if (!$answer) {
176 $answer = new stdClass();
177 $answer->question = $question->id;
178 $answer->answer = '';
179 $answer->feedback = '';
180 $answer->id = $DB->insert_record('question_answers', $answer);
181 }
182
183 if (trim($answerdata) === '*') {
94a6d656 184 $answer->answer = '*';
185 } else {
69988ed4 186 $answer->answer = $this->apply_unit($answerdata, $units);
94a6d656 187 if ($answer->answer === false) {
188 $result->notice = get_string('invalidnumericanswer', 'quiz');
516cf3eb 189 }
94a6d656 190 }
191 $answer->fraction = $question->fraction[$key];
69988ed4
TH
192 $answer->feedback = $this->import_or_save_files($question->feedback[$key],
193 $context, 'question', 'answerfeedback', $answer->id);
fe6ce234 194 $answer->feedbackformat = $question->feedback[$key]['format'];
69988ed4 195 $DB->update_record('question_answers', $answer);
f34488b2 196
94a6d656 197 // Set up the options object
198 if (!$options = array_shift($oldoptions)) {
69988ed4 199 $options = new stdClass();
94a6d656 200 }
69988ed4
TH
201 $options->question = $question->id;
202 $options->answer = $answer->id;
94a6d656 203 if (trim($question->tolerance[$key]) == '') {
204 $options->tolerance = '';
205 } else {
92b36005 206 $options->tolerance = $this->apply_unit($question->tolerance[$key], $units);
94a6d656 207 if ($options->tolerance === false) {
208 $result->notice = get_string('invalidnumerictolerance', 'quiz');
209 }
210 }
69988ed4 211 if (isset($options->id)) {
0bcf8b6f 212 $DB->update_record('question_numerical', $options);
69988ed4 213 } else {
0bcf8b6f 214 $DB->insert_record('question_numerical', $options);
1fe641f7 215 }
216 }
516cf3eb 217
69988ed4
TH
218 // Delete any left over old answer records.
219 $fs = get_file_storage();
220 foreach($oldanswers as $oldanswer) {
221 $fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id);
222 $DB->delete_records('question_answers', array('id' => $oldanswer->id));
516cf3eb 223 }
69988ed4
TH
224 foreach($oldoptions as $oldoption) {
225 $DB->delete_records('question_numerical', array('id' => $oldoption->id));
c5da9906 226 }
69988ed4
TH
227
228 $result = $this->save_numerical_options($question);
229 if (!empty($result->error) || !empty($result->notice)) {
1fe641f7 230 return $result;
231 }
69988ed4 232
1fe641f7 233 return true;
516cf3eb 234 }
235
92b36005
PP
236 /**
237 * The numerical options control the display and the grading of the unit
238 * part of the numerical question and related types (calculateds)
239 * Questions previous to 2,0 do not have this table as multianswer questions
240 * in all versions including 2,0. The default values are set to give the same grade
241 * as old question.
242 *
243 */
69988ed4 244 function save_numerical_options($question) {
c5da9906 245 global $DB;
fe6ce234 246
c5da9906 247 $result = new stdClass;
69988ed4 248
aeb15530 249 $update = true ;
fe6ce234 250 $options = $DB->get_record('question_numerical_options', array('question' => $question->id));
c5da9906 251 if (!$options) {
c5da9906 252 $options = new stdClass;
253 $options->question = $question->id;
69988ed4
TH
254 $options->instructions = '';
255 $options->id = $DB->insert_record('question_numerical_options', $options);
c5da9906 256 }
69988ed4
TH
257
258 if (isset($question->options->unitgradingtype)) {
92b36005 259 $options->unitgradingtype = $question->options->unitgradingtype;
69988ed4 260 } else {
c5da9906 261 $options->unitgradingtype = 0 ;
262 }
69988ed4 263 if (isset($question->unitpenalty)){
c5da9906 264 $options->unitpenalty = $question->unitpenalty;
69988ed4 265 } else { //so this is either an old question or a close question type
92b36005 266 $options->unitpenalty = 1 ;
c5da9906 267 }
cf146692 268 // if we came from the form then 'unitrole' exists
69988ed4 269 if (isset($question->unitrole)){
92b36005
PP
270 switch ($question->unitrole){
271 case '0' : $options->showunits = NUMERICALQUESTIONUNITNODISPLAY ;
272 break ;
273 case '1' : $options->showunits = NUMERICALQUESTIONUNITTEXTDISPLAY ;
274 break ;
275 case '2' : $options->showunits = NUMERICALQUESTIONUNITTEXTINPUTDISPLAY ;
276 $options->unitgradingtype = 0 ;
277 break ;
278 case '3' : $options->showunits = $question->multichoicedisplay ;
279 $options->unitgradingtype = $question->unitgradingtypes ;
280 break ;
cf146692 281 }
92b36005 282 } else {
69988ed4 283 if (isset($question->showunits)){
cf146692 284 $options->showunits = $question->showunits;
69988ed4 285 } else {
cf146692 286 if ($defaultunit = $this->get_default_numerical_unit($question)) {
92b36005 287 // so units can be used
04e91671 288 $options->showunits = NUMERICALQUESTIONUNITTEXTINPUTDISPLAY ;
69988ed4 289 } else {
cf146692 290 // only numerical will be graded
04e91671 291 $options->showunits = NUMERICALQUESTIONUNITNODISPLAY ;
cf146692
PP
292 }
293 }
c5da9906 294 }
69988ed4
TH
295
296 if (isset($question->unitsleft)) {
c5da9906 297 $options->unitsleft = $question->unitsleft;
69988ed4 298 } else {
c5da9906 299 $options->unitsleft = 0 ;
300 }
22f17bca 301
69988ed4
TH
302 $options->instructions = $this->import_or_save_files($question->instructions,
303 $question->context, 'qtype_' . $question->qtype, 'instructions', $options->id);
304 $options->instructionsformat = $question->instructions['format'];
305
306 $DB->update_record('question_numerical_options', $options);
fe6ce234 307
c5da9906 308 return $result;
309 }
310
516cf3eb 311 function save_numerical_units($question) {
f34488b2 312 global $DB;
ed0ba6da 313 $result = new stdClass;
516cf3eb 314
ed0ba6da 315 // Delete the units previously saved for this question.
f34488b2 316 $DB->delete_records('question_numerical_units', array('question' => $question->id));
ed0ba6da 317
26b26662 318 // Nothing to do.
319 if (!isset($question->multiplier)) {
320 $result->units = array();
321 return $result;
322 }
323
ed0ba6da 324 // Save the new units.
516cf3eb 325 $units = array();
cf146692 326 $unitalreadyinsert = array();
ed0ba6da 327 foreach ($question->multiplier as $i => $multiplier) {
516cf3eb 328 // Discard any unit which doesn't specify the unit or the multiplier
de08803f
PS
329 if (!empty($question->multiplier[$i]) && !empty($question->unit[$i])&& !array_key_exists($question->unit[$i],$unitalreadyinsert)) {
330 $unitalreadyinsert[$question->unit[$i]] = 1 ;
ed0ba6da 331 $units[$i] = new stdClass;
516cf3eb 332 $units[$i]->question = $question->id;
92b36005 333 $units[$i]->multiplier = $this->apply_unit($question->multiplier[$i], array());
516cf3eb 334 $units[$i]->unit = $question->unit[$i];
979425b5 335 $DB->insert_record('question_numerical_units', $units[$i]);
516cf3eb 336 }
337 }
338 unset($question->multiplier, $question->unit);
339
516cf3eb 340 $result->units = &$units;
341 return $result;
342 }
343
c5da9906 344 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
345 $state->responses = array();
346 $state->responses['answer'] = '';
347 $state->responses['unit'] = '';
fe6ce234 348
c5da9906 349 return true;
350 }
351 function restore_session_and_responses(&$question, &$state) {
352 if(false === strpos($state->responses[''], '|||||')){
aeb15530 353 $state->responses['answer']= $state->responses[''];
c5da9906 354 $state->responses['unit'] = '';
1c5299bf 355 $this->split_old_answer($state->responses[''], $question->options->units, $state->responses['answer'] ,$state->responses['unit'] );
c5da9906 356 }else {
357 $responses = explode('|||||', $state->responses['']);
358 $state->responses['answer']= $responses[0];
359 $state->responses['unit'] = $responses[1];
360 }
c5da9906 361
c5da9906 362 return true;
363 }
364
365 function find_unit_index(&$question,$value){
366 $length = 0;
367 $goodkey = 0 ;
368 foreach ($question->options->units as $key => $unit){
369 if($unit->unit ==$value ) {
370 return $key ;
371 }
aeb15530 372 }
c5da9906 373 return 0 ;
374 }
375
376 function split_old_answer($rawresponse, $units, &$answer ,&$unit ) {
377 $answer = $rawresponse ;
378 // remove spaces and normalise decimal places.
379 $search = array(' ', ',');
380 $replace = array('', '.');
381 $rawresponse = str_replace($search, $replace, trim($rawresponse));
382 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
383 $rawresponse, $responseparts)) {
fe6ce234 384 if(isset($responseparts[5]) ){
49580a59
PP
385 $unit = $responseparts[5] ;
386 }
fe6ce234 387 if(isset($responseparts[1]) ){
49580a59
PP
388 $answer = $responseparts[1] ;
389 }
c5da9906 390 }
391 return ;
392 }
393
394
395 function save_session_and_responses(&$question, &$state) {
396 global $DB;
c5da9906 397
398 $responses = '';
399 if(isset($state->responses['unit']) && isset($question->options->units[$state->responses['unit']])){
400 $responses = $state->responses['answer'].'|||||'.$question->options->units[$state->responses['unit']]->unit;
401 }else if(isset($state->responses['unit'])){
402 $responses = $state->responses['answer'].'|||||'.$state->responses['unit'] ;
403 }else {
404 $responses = $state->responses['answer'].'|||||';
405 }
406 // Set the legacy answer field
f685e830 407 $DB->set_field('question_states', 'answer', $responses, array('id' => $state->id));
c5da9906 408 return true;
409 }
410
9203b705 411 function delete_question($questionid, $contextid) {
f34488b2 412 global $DB;
9203b705
TH
413 $DB->delete_records('question_numerical', array('question' => $questionid));
414 $DB->delete_records('question_numerical_options', array('question' => $questionid));
415 $DB->delete_records('question_numerical_units', array('question' => $questionid));
416
417 parent::delete_question($questionid, $contextid);
516cf3eb 418 }
9203b705 419
f1ee48c8
PP
420 /**
421 * This function has been reinserted in numerical/questiontype.php to simplify
422 * the separate rendering of number and unit
423 */
424 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
92b36005 425 global $CFG, $OUTPUT;
92b36005 426
fe6ce234 427 $context = $this->get_context_by_category_id($question->category);
f1ee48c8
PP
428 $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
429 $formatoptions = new stdClass;
430 $formatoptions->noclean = true;
431 $formatoptions->para = false;
432 $nameprefix = $question->name_prefix;
fe6ce234
DC
433 $component = 'qtype_' . $question->qtype;
434 // rewrite instructions text
69988ed4 435 $question->options->instructions = quiz_rewrite_question_urls($question->options->instructions, 'pluginfile.php', $context->id, $component, 'instructions', array($state->attempt, $state->question), $question->id);
f1ee48c8
PP
436
437 /// Print question text and media
438
439 $questiontext = format_text($question->questiontext,
440 $question->questiontextformat,
441 $formatoptions, $cmoptions->course);
f1ee48c8
PP
442
443 /// Print input controls
444 // as the entry is controlled the question type here is numerical
445 // In all cases there is a text input for the number
fe6ce234 446 // If $question->options->showunits == NUMERICALQUESTIONUNITTEXTDISPLAY
f1ee48c8
PP
447 // there is an additional text input for the unit
448 // If $question->options->showunits == NUMERICALQUESTIONUNITMULTICHOICEDISPLAY"
449 // radio elements display the defined unit
450 // The code allows the input number elememt to be displayed
451 // before i.e. at left or after at rigth of the unit variants.
fe6ce234 452 $nameanswer = "name=\"".$question->name_prefix."answer\"";
f1ee48c8 453 $nameunit = "name=\"".$question->name_prefix."unit\"";
92b36005 454 // put old answer data in $state->responses['answer'] and $state->responses['unit']
46a8f8ee 455 if (isset($state->responses['']) && $state->responses[''] != '' && !isset($state->responses['answer'])){
b79c7c28
PP
456 $this->split_old_answer($state->responses[''], $question->options->units, $state->responses['answer'] ,$state->responses['unit'] );
457 }
92b36005 458 // prepare the values of the input elements to be dispalyed answer i.e. number and unit
f1ee48c8
PP
459 if (isset($state->responses['answer']) && $state->responses['answer']!='') {
460 $valueanswer = ' value="'.s($state->responses['answer']).'" ';
461 } else {
462 $valueanswer = ' value="" ';
463 }
464 if (isset($state->responses['unit']) && $state->responses['unit']!='') {
465 $valueunit = ' value="'.s($state->responses['unit']).'" ';
fe6ce234 466 } else {
f1ee48c8
PP
467 $valueunit = ' value="" ';
468 if ($question->options->showunits == NUMERICALQUESTIONUNITTEXTDISPLAY ){
469 $valueunit = ' value="'.s($question->options->units[0]->unit).'" ';
470 }
471 }
472
473 $feedback = '';
474 $class = '';
1285531b 475 $classunit = '' ;
eb1284a2 476 $classunitvalue = '' ;
f1ee48c8
PP
477 $feedbackimg = '';
478 $feedbackimgunit = '' ;
479 $answerasterisk = false ;
1285531b 480 $response = '' ;
eb1284a2 481 $valid_numerical_unit = false ;
92b36005
PP
482 $valid_numerical_unit_index = -1 ;
483 $unit_in_numerical_answer = false ;
eb1284a2 484 $rawgrade = 0 ;
f1ee48c8
PP
485 if ($options->feedback) {
486 $class = question_get_feedback_class(0);
1285531b 487 $classunit = question_get_feedback_class(0);
f1ee48c8
PP
488 $feedbackimg = question_get_feedback_image(0);
489 $feedbackimgunit = question_get_feedback_image(0);
490 $classunitvalue = 0 ;
92b36005
PP
491 $valid_numerical_unit_index = -1 ;
492 // if there is unit in answer and unitgradingtype = 0
493 // the grade is 0
f1ee48c8
PP
494 //this is OK for the first answer with a good response
495 // having to test for * so response as long as not empty
92b36005
PP
496 // $response = $this->extract_numerical_response($state->responses['answer']);
497 // test for a greater than 0 grade
f1ee48c8 498 foreach($question->options->answers as $answer) {
92b36005
PP
499 if ($this->test_response($question, $state, $answer)) {
500 // Answer was correct or partially correct.
501 if ( $answer->answer === '*'){
502 $answerasterisk = true ;
503 }
504 // in all cases
eb1284a2
PP
505 $class = question_get_feedback_class($answer->fraction);
506 $feedbackimg = question_get_feedback_image($answer->fraction);
92b36005
PP
507 if ($question->options->unitgradingtype == 0 || ($question->options->unitgradingtype == 0 && $answer->answer === '*')){
508 // if * then unit has the $answer->fraction value
509 // if $question->options->unitgradingtype == 0 everything has been checked
510 // if $question->options->showunits == NUMERICALQUESTIONUNITTEXTINPUTDISPLAY
511 // then number - unit combination has been used to test response
512 // so the unit should have same color
513 $classunit = question_get_feedback_class($answer->fraction);
514 $feedbackimgunit = question_get_feedback_image($answer->fraction);
515 $rawgrade = $answer->fraction ;
516
517
518 }else {
d8741145
PP
519 /* so we need to apply unit grading i.e. to check if the number-unit combination
520 * was the rigth one
521 * on NUMERICALQUESTIONUNITTEXTINPUTDISPLAY we need only to ckeck if applyunit will test OK
522 * with the $state->responses['unit'] value which cannot be empty
523 * if $state->responses['unit']
524 * if apply-unit is true with a specific unit as long as the unit as been written either in the
525 * we need the numerical response and test it with the available units
526 * if the unit used is good then it should be set OK
527 * however the unit could have been put in the number element in this case
528 * the unit penalty should be apllied.
529 * testing apply_unit with no units will get us a false response if there is any text in it
530 * testing apply_unit with a given unit will get a good value if the number is good with this unit
531 * apply unit will return the numerical if
532 * we need to know which conditions let to a good numerical value that were done in the
533 */
92b36005 534 $valid_numerical_unit = false ;
92b36005
PP
535 $rawgrade = $answer->fraction ;
536 $valid_numerical_unit_index = -1 ;
537 $invalid_unit_in_numerical_answer = false ;
538 if ( $answerasterisk ) {
539 $classunit = question_get_feedback_class($answer->fraction);
540 $feedbackimgunit = question_get_feedback_image($answer->fraction);
541 $valid_numerical_unit = true ;//everything is true with *
92b36005 542 } else {
92b36005
PP
543 // if( isset($state->responses['unit']) && $state->responses['unit'] != '' ){// unit should be written in the unit input or checked in multichoice
544 // we need to see if something was written in the answer field that was not in the number
545 // although we cannot actually detect units put before the number which will cause bad numerical.
546 // use extract response
547 $response = $this->extract_numerical_response($state->responses['answer']);
548 if(isset($response->unit ) && $response->unit != ''){
549 $unit_in_numerical_answer = true ;
fe6ce234 550 }else {
92b36005 551 $unit_in_numerical_answer = false ;
eb1284a2 552 }
92b36005
PP
553
554 // the we let the testing to the two cases either
555 // NUMERICALQUESTIONUNITTEXTINPUTDISPLAY or
556 // NUMERICALQUESTIONUNITMULTICHOICEDISPLAY
557 if( !isset($state->responses['unit']) || $state->responses['unit'] == '' ){
558 // unit should be written in the unit input or checked in multichoice
559 $valid_numerical_unit = false ;
560 $classunit = question_get_feedback_class(0);
561 $feedbackimgunit = question_get_feedback_image(0);
562 $empty_unit = true ;
563 } else {
564 // echo"<p> some unit answer <pre>";print_r($answer) ;echo"</pre></p>";
565 // echo"<p> some unit answer <pre>";print_r($answer) ;echo"</pre></p>";
566 $empty_unit = false ;
567 $valid_numerical_unit = false ;
568
569 foreach ($question->options->units as $key => $unit) {
570 if ($unit->unit == $state->responses['unit']){
571 // $response = $this->apply_unit($state->responses['answer'].$unit->unit, array($question->options->units[$key])) ;
572 // echo "<p> avant false valid_numerical_unit_index $valid_numerical_unit_index ".$state->responses['answer']."</p>";
573 $invalid_unit_found = 0 ;
574 if ($response->number !== false) {
575 //echo "<p> avanr get valid_numerical_unit_index $valid_numerical_unit_index </p>";
576 // $this->get_tolerance_interval($answer);
577 $testresponse = $response->number /$unit->multiplier ;
578 if($answer->min <= $testresponse && $testresponse <= $answer->max){
579 //echo "<p> apres min max valid_numerical_unit_index $valid_numerical_unit_index </p>";
580 $classunit = question_get_feedback_class($answer->fraction) ; //question_get_feedback_class(1);
581 $feedbackimgunit = question_get_feedback_image($rawgrade);
582 $valid_numerical_unit = true ;
92b36005
PP
583 $valid_numerical_unit_index = $key ;
584 break ;
585 }
586 }
a9efae50 587 }
92b36005 588 }
92b36005 589 }
a9efae50 590 }
f1ee48c8 591 }
f1ee48c8 592 if ($answer->feedback) {
92b36005 593 $answer->feedback = quiz_rewrite_question_urls($answer->feedback, 'pluginfile.php', $context->id, 'question', 'answerfeedback', array($state->attempt, $state->question), $answer->id);
a9efae50 594 $feedback = format_text($answer->feedback, $answer->feedbackformat, $formatoptions, $cmoptions->course);
f1ee48c8 595 }
92b36005 596
fe6ce234
DC
597 break;
598 }
f1ee48c8 599 }
92b36005 600 }
eb1284a2
PP
601 $state->options->raw_unitpenalty = 0 ;
602 $raw_unitpenalty = 0 ;
fe6ce234 603 if( $question->options->showunits == NUMERICALQUESTIONUNITNODISPLAY ||
eb1284a2
PP
604 $question->options->showunits == NUMERICALQUESTIONUNITTEXTDISPLAY ) {
605 $classunitvalue = 1 ;
606 }
fe6ce234 607
92b36005 608 if(! $answerasterisk && $question->options->unitgradingtype != 0 && (! $valid_numerical_unit || $unit_in_numerical_answer)){
eb1284a2
PP
609 if($question->options->unitgradingtype == 1){
610 $raw_unitpenalty = $question->options->unitpenalty * $rawgrade ;
611 }else {
612 $raw_unitpenalty = $question->options->unitpenalty * $question->maxgrade;
613 }
fe6ce234 614 $state->options->raw_unitpenalty = $raw_unitpenalty ;
eb1284a2
PP
615 }
616
f1ee48c8
PP
617 /// Removed correct answer, to be displayed later MDL-7496
618 include("$CFG->dirroot/question/type/numerical/display.html");
619 }
aeb15530 620
516cf3eb 621
5a14d563 622 function compare_responses(&$question, $state, $teststate) {
aa384ade 623
92b36005 624 if ($question->options->showunits == NUMERICALQUESTIONUNITMULTICHOICEDISPLAY && isset($question->options->units) && isset($state->responses['unit']) && isset($question->options->units[$state->responses['unit']] )){
c5da9906 625 $state->responses['unit']=$question->options->units[$state->responses['unit']]->unit;
626 };
627
aeb15530 628
c5da9906 629 $responses = '';
630 $testresponses = '';
631 if (isset($state->responses['answer'])){
632 $responses = $state->responses['answer'];
633 }
634 if (isset($state->responses['unit'])){
635 $responses .= $state->responses['unit'];
636 }
637 if (isset($teststate->responses['answer'])){
638 $testresponses = $teststate->responses['answer'];
639 }
640 if (isset($teststate->responses['unit'])){
641 $testresponses .= $teststate->responses['unit'];
642 }
8b831fbb 643
f0b6151c 644 if ( isset($responses) && isset($testresponses )) {
c5da9906 645
646 return $responses == $testresponses ;
5a14d563 647 }
648 return false;
516cf3eb 649 }
650
1fe641f7 651 /**
652 * Checks whether a response matches a given answer, taking the tolerance
f1ee48c8
PP
653 * and but NOT the unit into account. Returns a true for if a response matches the
654 * answer or in one of the unit , false if it doesn't.
655 * the total grading will see if the unit match.
fe6ce234 656 * if unit != -1 then the test is done only on this unit
1fe641f7 657 */
f1ee48c8 658 function test_response(&$question, &$state, $answer ) {
55894a42 659 // Deal with the match anything answer.
fac1189d 660 if ($answer->answer === '*') {
55894a42 661 return true;
516cf3eb 662 }
92b36005
PP
663 // using old grading process if $question->unitgradingtype == 0
664 // and adding unit1 for the new option NUMERICALQUESTIONUNITTEXTDISPLAY
665 if ($question->options->unitgradingtype == 0 ){
666 // values coming form old question stored in attempts
667 if (!isset($state->responses['answer']) && isset($state->responses[''])){
668 $state->responses['answer'] = $state->responses[''];
f1ee48c8 669 }
92b36005
PP
670 $answertotest = $state->responses['answer'];
671 // values coming from NUMERICALQUESTIONUNITTEXTINPUTDISPLAY
672 // or NUMERICALQUESTIONUNITTEXTDISPLAY as unit hidden HTML element
8f609a37
PP
673
674 if($question->options->showunits == NUMERICALQUESTIONUNITTEXTINPUTDISPLAY ){
d8741145 675
8f609a37
PP
676 $testresponse = $this->extract_numerical_response($state->responses['answer']);
677 if($testresponse->unit != '' || $testresponse->number === false){
678 return false;
679 }
680 $answertotest = $testresponse->number ;
681 }
92b36005 682 if(isset($state->responses['unit'])) {
8f609a37 683 $answertotest .= $state->responses['unit'] ;
92b36005
PP
684 }
685 // if ($question->options->showunits == NUMERICALQUESTIONUNITTEXTDISPLAY && isset($question->options->units[0])){
686 // $answertotest .= $question->options->units[0]->unit ;
687 // }
688 // test OK if only numerical or numerical with known unit names with the unit mltiplier applied
689 $response = $this->apply_unit($answertotest, $question->options->units);
92b36005
PP
690
691 if ($response === false) {
692 return false; // The student did not type a number.
693 }
694
695 // The student did type a number, so check it with tolerances.
696 $this->get_tolerance_interval($answer);
92b36005 697 return ($answer->min <= $response && $response <= $answer->max);
5d548d3e 698 } else { // $question->options->unitgradingtype > 0
92b36005
PP
699 /* testing with unitgradingtype $question->options->unitgradingtype > 0
700 * if the response is at least patially true
701 * if the numerical value agree in the interval
702 * if so the only non valid case will be a bad unit and a unity penalty.
703
704 To be able to test (old) questions that do not have an unit
705 * input element the test is done using the $state->responses['']
706 * which contains the response which is analyzed by extract_numerical_response()
707 * If the data comes from the numerical or calculated display
708 * the $state->responses['unit'] comes from either
709 * a multichoice radio element NUMERICALQUESTIONUNITMULTICHOICEDISPLAY
710 * where the $state->responses['unit'] value is the key => unit object
711 * in the the $question->options->units array
712 * or an input text element NUMERICALQUESTIONUNITTEXTINPUTDISPLAY
713 * which contains the student response
714 * for NUMERICALQUESTIONUNITTEXTDISPLAY and NUMERICALQUESTIONUNITNODISPLAY
715 *
716 */
717
718 $response = $this->extract_numerical_response($state->responses['answer']);
719
92b36005
PP
720
721 if ($response->number === false ) {
722 return false; // The student did not type a number.
723 }
724
725 // The student did type a number, so check it with tolerances.
726 $this->get_tolerance_interval($answer);
727 if ($answer->min <= $response->number && $response->number <= $answer->max){
92b36005
PP
728 return true;
729 }
730 // testing for other units
731 if ( isset($question->options->units) && count($question->options->units) > 0) {
732 foreach($question->options->units as $key =>$unit){
733 $testresponse = $response->number /$unit->multiplier ;
734 if($answer->min <= $testresponse && $testresponse<= $answer->max) {
735 return true;
736 }
737 }
738 }
739 return false;
f1ee48c8
PP
740 }
741 return false;
516cf3eb 742 }
743
04e91671 744 /**
fe6ce234 745 * Performs response processing and grading
04e91671 746 * The function was redefined for handling correctly the two parts
fe6ce234
DC
747 * number and unit of numerical or calculated questions
748 * The code handles also the case when there no unit defined by the user or
749 * when used in a multianswer (Cloze) question.
04e91671
PP
750 * This function performs response processing and grading and updates
751 * the state accordingly.
752 * @return boolean Indicates success or failure.
753 * @param object $question The question to be graded. Question type
754 * specific information is included.
755 * @param object $state The state of the question to grade. The current
756 * responses are in ->responses. The last graded state
757 * is in ->last_graded (hence the most recently graded
758 * responses are in ->last_graded->responses). The
759 * question type specific information is also
760 * included. The ->raw_grade and ->penalty fields
761 * must be updated. The method is able to
762 * close the question session (preventing any further
763 * attempts at this question) by setting
764 * $state->event to QUESTION_EVENTCLOSEANDGRADE
765 * @param object $cmoptions
766 */
f1ee48c8 767 function grade_responses(&$question, &$state, $cmoptions) {
92b36005
PP
768 if ( isset($state->responses['']) && $state->responses[''] != '' && !isset($state->responses['answer'])){
769 $this->split_old_answer($state->responses[''], $question->options->units, $state->responses['answer'] ,$state->responses['unit'] );
f1ee48c8 770 }
fe6ce234 771
c5da9906 772 $state->raw_grade = 0;
eb1284a2 773 $valid_numerical_unit = false ;
f1ee48c8
PP
774 $break = 0 ;
775 $unittested = '';
776 $hasunits = 0 ;
f1ee48c8 777 $answerasterisk = false ;
fe6ce234
DC
778
779 $break = 0 ;
c5da9906 780 foreach($question->options->answers as $answer) {
92b36005
PP
781 if ($this->test_response($question, $state, $answer)) {
782 // Answer was correct or partially correct.
783 $state->raw_grade = $answer->fraction ;
784 if ($question->options->unitgradingtype == 0 || $answer->answer === '*'){
785 // if * then unit has the $answer->fraction value
786 // if $question->options->unitgradingtype == 0 everything has been checked
787 // if $question->options->showunits == NUMERICALQUESTIONUNITTEXTINPUTDISPLAY
788 // then number - unit combination has been used to test response
789 // so the unit should have same color
790
791 }else {
792 // so we need to apply unit grading i.e. to check if the number-unit combination
793 // was the rigth one
794 $valid_numerical_unit = false ;
795 $class = question_get_feedback_class($answer->fraction);
796 $feedbackimg = question_get_feedback_image($answer->fraction);
797 if(isset($state->responses['unit']) && $state->responses['unit'] != '' ){
798 foreach ($question->options->units as $key => $unit) {
799 if ($unit->unit == $state->responses['unit']){
800
801 $response = $this->apply_unit($state->responses['answer'].$state->responses['unit'], array($question->options->units[$key])) ;
802 if ($response !== false) {
803 $this->get_tolerance_interval($answer);
804 if($answer->min <= $response && $response <= $answer->max){
805 $valid_numerical_unit = true ;
806 }
807 }
808 break ;
809 }
810 }
fe6ce234 811 }
f1ee48c8 812 }
92b36005
PP
813 break ;
814 }
0a49ee5c 815 }
c5da9906 816 // apply unit penalty
eb1284a2 817 $raw_unitpenalty = 0 ;
92b36005 818 if($question->options->unitgradingtype != 0 && !empty($question->options->unitpenalty)&& $valid_numerical_unit != true ){
c5da9906 819 if($question->options->unitgradingtype == 1){
eb1284a2 820 $raw_unitpenalty = $question->options->unitpenalty * $state->raw_grade ;
c5da9906 821 }else {
eb1284a2 822 $raw_unitpenalty = $question->options->unitpenalty * $question->maxgrade;
c5da9906 823 }
eb1284a2 824 $state->raw_grade -= $raw_unitpenalty ;
c5da9906 825 }
92b36005 826
c5da9906 827 // Make sure we don't assign negative or too high marks.
c5da9906 828 $state->raw_grade = min(max((float) $state->raw_grade,
829 0.0), 1.0) * $question->maxgrade;
92b36005 830
c5da9906 831 // Update the penalty.
832 $state->penalty = $question->penalty * $question->maxgrade;
833
834 // mark the state as graded
835 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
836
837 return true;
838 }
aeb15530
PS
839
840
516cf3eb 841 function get_correct_responses(&$question, &$state) {
842 $correct = parent::get_correct_responses($question, $state);
c85aea03 843 $unit = $this->get_default_numerical_unit($question);
844 if (isset($correct['']) && $correct[''] != '*' && $unit) {
516cf3eb 845 $correct[''] .= ' '.$unit->unit;
aeb15530 846 }
516cf3eb 847 return $correct;
848 }
849
850 // ULPGC ecastro
851 function get_all_responses(&$question, &$state) {
1fe641f7 852 $result = new stdClass;
853 $answers = array();
516cf3eb 854 $unit = $this->get_default_numerical_unit($question);
855 if (is_array($question->options->answers)) {
856 foreach ($question->options->answers as $aid=>$answer) {
1fe641f7 857 $r = new stdClass;
516cf3eb 858 $r->answer = $answer->answer;
859 $r->credit = $answer->fraction;
860 $this->get_tolerance_interval($answer);
55894a42 861 if ($r->answer != '*' && $unit) {
862 $r->answer .= ' ' . $unit->unit;
516cf3eb 863 }
864 if ($answer->max != $answer->min) {
865 $max = "$answer->max"; //format_float($answer->max, 2);
866 $min = "$answer->min"; //format_float($answer->max, 2);
867 $r->answer .= ' ('.$min.'..'.$max.')';
868 }
869 $answers[$aid] = $r;
870 }
516cf3eb 871 }
872 $result->id = $question->id;
873 $result->responses = $answers;
874 return $result;
875 }
c5da9906 876 function get_actual_response($question, $state) {
aeb15530 877 if (!empty($state->responses) && !empty($state->responses[''])) {
c5da9906 878 if(false === strpos($state->responses[''], '|||||')){
879 $responses[] = $state->responses[''];
880 }else {
881 $resp = explode('|||||', $state->responses['']);
aeb15530 882 $responses[] = $resp[0].$resp[1];
c5da9906 883 }
884 } else {
885 $responses[] = '';
886 }
aeb15530 887
c5da9906 888 return $responses;
889 }
890
516cf3eb 891
892 function get_tolerance_interval(&$answer) {
893 // No tolerance
894 if (empty($answer->tolerance)) {
1fe641f7 895 $answer->tolerance = 0;
516cf3eb 896 }
897
898 // Calculate the interval of correct responses (min/max)
899 if (!isset($answer->tolerancetype)) {
900 $answer->tolerancetype = 2; // nominal
901 }
902
223ad0b9 903 // We need to add a tiny fraction depending on the set precision to make the
516cf3eb 904 // comparison work correctly. Otherwise seemingly equal values can yield
905 // false. (fixes bug #3225)
223ad0b9 906 $tolerance = (float)$answer->tolerance + ("1.0e-".ini_get('precision'));
516cf3eb 907 switch ($answer->tolerancetype) {
908 case '1': case 'relative':
909 /// Recalculate the tolerance and fall through
910 /// to the nominal case:
911 $tolerance = $answer->answer * $tolerance;
dcd4192a 912 // Do not fall through to the nominal case because the tiny fraction is a factor of the answer
913 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
914 $max = $answer->answer + $tolerance;
915 $min = $answer->answer - $tolerance;
916 break;
516cf3eb 917 case '2': case 'nominal':
918 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
dcd4192a 919 // $answer->tolerance 0 or something else
920 if ((float)$answer->tolerance == 0.0 && abs((float)$answer->answer) <= $tolerance ){
f34488b2 921 $tolerance = (float) ("1.0e-".ini_get('precision')) * abs((float)$answer->answer) ; //tiny fraction
dcd4192a 922 } else if ((float)$answer->tolerance != 0.0 && abs((float)$answer->tolerance) < abs((float)$answer->answer) && abs((float)$answer->answer) <= $tolerance){
f34488b2 923 $tolerance = (1+("1.0e-".ini_get('precision')) )* abs((float) $answer->tolerance) ;//tiny fraction
924 }
925
516cf3eb 926 $max = $answer->answer + $tolerance;
927 $min = $answer->answer - $tolerance;
928 break;
ed0ba6da 929 case '3': case 'geometric':
516cf3eb 930 $quotient = 1 + abs($tolerance);
931 $max = $answer->answer * $quotient;
932 $min = $answer->answer / $quotient;
933 break;
934 default:
0b4f4187 935 print_error('unknowntolerance', 'question', '', $answer->tolerancetype);
516cf3eb 936 }
937
938 $answer->min = $min;
939 $answer->max = $max;
940 return true;
941 }
942
943 /**
1fe641f7 944 * Checks if the $rawresponse has a unit and applys it if appropriate.
945 *
946 * @param string $rawresponse The response string to be converted to a float.
947 * @param array $units An array with the defined units, where the
948 * unit is the key and the multiplier the value.
949 * @return float The rawresponse with the unit taken into
950 * account as a float.
951 */
f1ee48c8 952 function extract_numerical_response($rawresponse) {
92b36005 953 $extractedresponse = new stdClass() ;
f1ee48c8
PP
954 $rawresponse = trim($rawresponse) ;
955 $search = array(' ', ',');
956 // test if a . is present or there are multiple , (i.e. 2,456,789 ) so that we don't need spaces and ,
957 if ( strpos($rawresponse,'.' ) !== false || substr_count($rawresponse,',') > 1 ) {
958 $replace = array('', '');
fe6ce234 959 }else { // remove spaces and normalise , to a . .
f1ee48c8
PP
960 $replace = array('', '.');
961 }
962 $rawresponse = str_replace($search, $replace, $rawresponse);
963
964 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
965 $rawresponse, $responseparts)) {
92b36005
PP
966 //return (float)$responseparts[1] ;
967 $extractedresponse->number = (float)$responseparts[1] ;
968 }else {
969 $extractedresponse->number = false ;
970 }
971 if (!empty($responseparts[5])) {
972 $extractedresponse->unit = $responseparts[5] ;
973 }else {
974 $extractedresponse->unit = '';
f1ee48c8 975 }
92b36005 976
f1ee48c8 977 // Invalid number. Must be wrong.
92b36005 978 return clone($extractedresponse) ;
f1ee48c8 979 }
eb1284a2 980 /**
f1ee48c8
PP
981 * Checks if the $rawresponse has a unit and applys it if appropriate.
982 *
983 * @param string $rawresponse The response string to be converted to a float.
984 * @param array $units An array with the defined units, where the
985 * unit is the key and the multiplier the value.
986 * @return float The rawresponse with the unit taken into
987 * account as a float.
988 */
92b36005 989 function apply_unit($rawresponse, $units) {
92b36005 990
516cf3eb 991 // Make units more useful
992 $tmpunits = array();
993 foreach ($units as $unit) {
994 $tmpunits[$unit->unit] = $unit->multiplier;
995 }
1fe641f7 996 // remove spaces and normalise decimal places.
be1bb80e 997 $rawresponse = trim($rawresponse) ;
516cf3eb 998 $search = array(' ', ',');
be1bb80e
PP
999 // test if a . is present or there are multiple , (i.e. 2,456,789 ) so that we don't need spaces and ,
1000 if ( strpos($rawresponse,'.' ) !== false || substr_count($rawresponse,',') > 1 ) {
1001 $replace = array('', '');
fe6ce234 1002 }else { // remove spaces and normalise , to a . .
be1bb80e
PP
1003 $replace = array('', '.');
1004 }
1005 $rawresponse = str_replace($search, $replace, $rawresponse);
f34488b2 1006
f1ee48c8 1007
1fe641f7 1008 // Apply any unit that is present.
f1ee48c8 1009 if (ereg('^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$',
1fe641f7 1010 $rawresponse, $responseparts)) {
92b36005 1011 // echo"<p> responseparts <pre>";print_r($responseparts) ;echo"</pre></p>";
f34488b2 1012
1fe641f7 1013 if (!empty($responseparts[5])) {
f34488b2 1014
1fe641f7 1015 if (isset($tmpunits[$responseparts[5]])) {
1016 // Valid number with unit.
1017 return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
c5da9906 1018 } else {
f1ee48c8
PP
1019 // Valid number with invalid unit. Must be wrong.
1020 return false;
c5da9906 1021 }
1022
1023 } else {
1024 // Valid number without unit.
1025 return (float)$responseparts[1];
1026 }
1027 }
1028 // Invalid number. Must be wrong.
1029 return false;
1030 }
f1ee48c8 1031
04e91671 1032 /**
5d548d3e
TH
1033 * function used in function definition_inner()
1034 * of edit_..._form.php for
1035 * numerical, calculated, calculatedsimple
1036 */
cf146692 1037 function add_units_options(&$mform, &$that){
cf146692 1038 // Units are graded
92b36005
PP
1039 $mform->addElement('header', 'unithandling', get_string('unitshandling', 'qtype_numerical'));
1040 $mform->addElement('radio', 'unitrole', get_string('unitnotused', 'qtype_numerical'), get_string('onlynumerical', 'qtype_numerical'),0);
92b36005
PP
1041 $mform->addElement('radio', 'unitrole', get_string('unitdisplay', 'qtype_numerical'), get_string('oneunitshown', 'qtype_numerical'),1);
1042 $mform->addElement('radio', 'unitrole', get_string('unitsused', 'qtype_numerical'), get_string('manynumerical', 'qtype_numerical'),2);
92b36005
PP
1043 $mform->addElement('static', 'separator2', '', '<HR/>');
1044 $mform->addElement('radio', 'unitrole', get_string('unitgraded1', 'qtype_numerical'), get_string('unitgraded', 'qtype_numerical'),3);
cf146692
PP
1045 $penaltygrp = array();
1046 $penaltygrp[] =& $mform->createElement('text', 'unitpenalty', get_string('unitpenalty', 'qtype_numerical') ,
1047 array('size' => 6));
1048 $unitgradingtypes = array('1' => get_string('decfractionofquestiongrade', 'qtype_numerical'), '2' => get_string('decfractionofresponsegrade', 'qtype_numerical'));
92b36005 1049 $penaltygrp[] =& $mform->createElement('select', 'unitgradingtypes', '' , $unitgradingtypes );
cf146692 1050 $mform->addGroup($penaltygrp, 'penaltygrp', get_string('unitpenalty', 'qtype_numerical'),' ' , false);
92b36005
PP
1051 $multichoicedisplaygrp = array();
1052 $multichoicedisplaygrp[] =& $mform->createElement('radio', 'multichoicedisplay', get_string('unitedit', 'qtype_numerical'), get_string('editableunittext', 'qtype_numerical'),0);
1053 $multichoicedisplaygrp[] =& $mform->createElement('radio', 'multichoicedisplay', get_string('selectunits', 'qtype_numerical') , get_string('unitchoice', 'qtype_numerical'),1);
d8741145 1054 $mform->addGroup($multichoicedisplaygrp, 'multichoicedisplaygrp', get_string('studentunitanswer', 'qtype_numerical'),' OR ' , false);
92b36005
PP
1055 $unitslefts = array('0' => get_string('rightexample', 'qtype_numerical'),'1' => get_string('leftexample', 'qtype_numerical'));
1056 $mform->addElement('select', 'unitsleft', get_string('unitposition', 'qtype_numerical') , $unitslefts );
5d548d3e 1057
92b36005 1058 $mform->addElement('static', 'separator2', '<HR/>', '<HR/>');
5d548d3e 1059
fe6ce234 1060 $mform->addElement('editor', 'instructions', get_string('instructions', 'qtype_numerical'), null, $that->editoroptions);
cf146692 1061 $showunits1grp = array();
cf146692 1062 $mform->addElement('static', 'separator2', '<HR/>', '<HR/>');
aeb15530 1063
c5da9906 1064 $mform->setType('unitpenalty', PARAM_NUMBER);
c5da9906 1065 $mform->setDefault('unitpenalty', 0.1);
92b36005
PP
1066 $mform->setDefault('unitgradingtypes', 1);
1067 $mform->addHelpButton('penaltygrp', 'unitpenalty', 'qtype_numerical'); // TODO help did not exist before MDL-21695
c5da9906 1068 $mform->setDefault('unitsleft', 0);
c5da9906 1069 $mform->setType('instructions', PARAM_RAW);
5fcefc97 1070 $mform->addHelpButton('instructions', 'numericalinstructions', 'qtype_numerical');
92b36005 1071 $mform->disabledIf('penaltygrp', 'unitrole','eq','0');
5fe5e8ac 1072 $mform->disabledIf('penaltygrp', 'unitrole','eq','1');
92b36005 1073 $mform->disabledIf('penaltygrp', 'unitrole','eq','2');
5d548d3e
TH
1074 $mform->disabledIf('unitsleft', 'unitrole','eq','0');
1075 $mform->disabledIf('multichoicedisplay','unitrole','eq','0');
1076 $mform->disabledIf('multichoicedisplay','unitrole','eq','1');
1077 $mform->disabledIf('multichoicedisplay','unitrole','eq','2');
c5da9906 1078 }
5fcefc97
DM
1079
1080 /**
1081 * function used in in function definition_inner()
1082 * of edit_..._form.php for
1083 * numerical, calculated, calculatedsimple
1084 */
fe6ce234 1085 function add_units_elements(& $mform,& $that) {
cf146692
PP
1086 $repeated = array();
1087 $repeated[] =& $mform->createElement('header', 'unithdr', get_string('unithdr', 'qtype_numerical', '{no}'));
1088
1089 $repeated[] =& $mform->createElement('text', 'unit', get_string('unit', 'quiz'));
1090 $mform->setType('unit', PARAM_NOTAGS);
1091
1092 $repeated[] =& $mform->createElement('text', 'multiplier', get_string('multiplier', 'quiz'));
1093 $mform->setType('multiplier', PARAM_NUMBER);
1094
04e91671 1095 if (isset($that->question->options)){
cf146692
PP
1096 $countunits = count($that->question->options->units);
1097 } else {
1098 $countunits = 0;
1099 }
1100 if ($that->question->formoptions->repeatelements){
1101 $repeatsatstart = $countunits + 1;
1102 } else {
1103 $repeatsatstart = $countunits;
1104 }
1105 $that->repeat_elements($repeated, $repeatsatstart, array(), 'nounits', 'addunits', 2, get_string('addmoreunitblanks', 'qtype_calculated', '{no}'));
1106
1107 if ($mform->elementExists('multiplier[0]')){
1108 $firstunit =& $mform->getElement('multiplier[0]');
1109 $firstunit->freeze();
1110 $firstunit->setValue('1.0');
1111 $firstunit->setPersistantFreeze(true);
5fcefc97 1112 $mform->addHelpButton('multiplier[0]', 'numericalmultiplier', 'qtype_numerical');
cf146692
PP
1113 }
1114 }
5fcefc97
DM
1115
1116 /**
fe6ce234
DC
1117 * function used in in function data_preprocessing() of edit_numerical_form.php for
1118 * numerical, calculated, calculatedsimple
1119 */
1120 function set_numerical_unit_data($mform, &$question, &$default_values){
1121
1122 list($categoryid) = explode(',', $question->category);
1123 $context = $this->get_context_by_category_id($categoryid);
f1ee48c8
PP
1124
1125 if (isset($question->options)){
92b36005
PP
1126 $default_values['unitgradingtypes'] = 1 ;
1127 if ($question->options->unitgradingtype == 2 ) {
1128 $default_values['unitgradingtypes'] = 1 ;
1129 }
1130 if ($question->options->unitgradingtype == 0 ) {
1131 $default_values['unitgradingtypes'] = 0 ;
1132 }
f1ee48c8
PP
1133 $default_values['unitpenalty'] = $question->options->unitpenalty ;
1134 switch ($question->options->showunits){
92b36005
PP
1135 case 0 :// NUMERICALQUESTIONUNITTEXTINPUTDISPLAY
1136 if($question->options->unitgradingtype == 0 ){
1137 $default_values['unitrole'] = 2 ;
1138 $default_values['multichoicedisplay'] = 0 ;
1139 }else { // 1 or 2
1140 $default_values['unitrole'] = 3 ;
1141 $default_values['multichoicedisplay'] = 0 ;
1142 $default_values['unitgradingtypes'] = $question->options->unitgradingtype ;
1143 }
1144 break;
1145 case 1 : // NUMERICALQUESTIONUNITMULTICHOICEDISPLAY
1146 $default_values['unitrole'] = 3 ;
1147 $default_values['multichoicedisplay'] = $question->options->unitgradingtype ;
1148 $default_values['unitgradingtypes'] = $question->options->unitgradingtype ;
f1ee48c8 1149 break;
92b36005 1150 case 2 : // NUMERICALQUESTIONUNITTEXTDISPLAY
f1ee48c8 1151 $default_values['unitrole'] = 1 ;
92b36005
PP
1152 case 3 : // NUMERICALQUESTIONUNITNODISPLAY
1153 $default_values['unitrole'] = 0 ;
1154 // $default_values['showunits1'] = $question->options->showunits ;
f1ee48c8 1155 break;
fe6ce234 1156 }
f1ee48c8 1157 $default_values['unitsleft'] = $question->options->unitsleft ;
fe6ce234
DC
1158
1159 // processing files
1160 $component = 'qtype_' . $question->qtype;
1161 $draftid = file_get_submitted_draft_itemid('instructions');
1162 $default_values['instructions'] = array();
1163 $default_values['instructions']['format'] = $question->options->instructionsformat;
1164 $default_values['instructions']['text'] = file_prepare_draft_area(
1165 $draftid, // draftid
1166 $context->id, // context
1167 $component, // component
1168 'instruction', // filarea
1169 !empty($question->id)?(int)$question->id:null, // itemid
1170 $mform->fileoptions, // options
1171 $question->options->instructions // text
1172 );
1173 $default_values['instructions']['itemid'] = $draftid;
1174
1175 if (isset($question->options->units)) {
f1ee48c8
PP
1176 $units = array_values($question->options->units);
1177 if (!empty($units)) {
1178 foreach ($units as $key => $unit){
1179 $default_values['unit['.$key.']'] = $unit->unit;
1180 $default_values['multiplier['.$key.']'] = $unit->multiplier;
1181 }
1182 }
1183 }
1184 }
1185 }
1186
fe6ce234
DC
1187 /**
1188 * function use in in function validation()
1189 * of edit_..._form.php for
1190 * numerical, calculated, calculatedsimple
1191 */
cf146692
PP
1192
1193 function validate_numerical_options(& $data, & $errors){
1194 $units = $data['unit'];
92b36005
PP
1195 switch ($data['unitrole']){
1196 case '0' : $showunits = NUMERICALQUESTIONUNITNODISPLAY ;
1197 break ;
1198 case '1' : $showunits = NUMERICALQUESTIONUNITTEXTDISPLAY ;
1199 break ;
1200 case '2' : $showunits = NUMERICALQUESTIONUNITTEXTINPUTDISPLAY ;
1201 break ;
1202 case '3' : $showunits = $data['multichoicedisplay'] ;
1203 break ;
1204 }
fe6ce234
DC
1205
1206 if (($showunits == NUMERICALQUESTIONUNITTEXTINPUTDISPLAY) ||
1207 ($showunits == NUMERICALQUESTIONUNITMULTICHOICEDISPLAY ) ||
04e91671 1208 ($showunits == NUMERICALQUESTIONUNITTEXTDISPLAY )){
cf146692
PP
1209 if (trim($units[0]) == ''){
1210 $errors['unit[0]'] = 'You must set a valid unit name' ;
1211 }
1212 }
04e91671 1213 if ($showunits == NUMERICALQUESTIONUNITNODISPLAY ){
cf146692
PP
1214 if (count($units)) {
1215 foreach ($units as $key => $unit){
1216 if ($units[$key] != ''){
1217 $errors["unit[$key]"] = 'You must erase this unit name' ;
1218 }
1219 }
1220 }
1221 }
fe6ce234
DC
1222
1223
cf146692
PP
1224 // Check double units.
1225 $alreadyseenunits = array();
1226 if (isset($data['unit'])) {
1227 foreach ($data['unit'] as $key => $unit) {
1228 $trimmedunit = trim($unit);
1229 if ($trimmedunit!='' && in_array($trimmedunit, $alreadyseenunits)) {
1230 $errors["unit[$key]"] = get_string('errorrepeatedunit', 'qtype_numerical');
1231 if (trim($data['multiplier'][$key]) == '') {
1232 $errors["multiplier[$key]"] = get_string('errornomultiplier', 'qtype_numerical');
1233 }
1234 } elseif($trimmedunit!='') {
1235 $alreadyseenunits[] = $trimmedunit;
1236 }
1237 }
1238 }
1239 $units = $data['unit'];
1240 if (count($units)) {
1241 foreach ($units as $key => $unit){
1242 if (is_numeric($unit)){
1243 $errors['unit['.$key.']'] = get_string('mustnotbenumeric', 'qtype_calculated');
1244 }
1245 $trimmedunit = trim($unit);
1246 $trimmedmultiplier = trim($data['multiplier'][$key]);
1247 if (!empty($trimmedunit)){
1248 if (empty($trimmedmultiplier)){
1249 $errors['multiplier['.$key.']'] = get_string('youmustenteramultiplierhere', 'qtype_calculated');
1250 }
1251 if (!is_numeric($trimmedmultiplier)){
1252 $errors['multiplier['.$key.']'] = get_string('mustbenumeric', 'qtype_calculated');
1253 }
1254
1255 }
fe6ce234 1256 }
cf146692
PP
1257 }
1258
1259 }
92b36005 1260
aeb15530 1261
04e91671 1262 function valid_unit($rawresponse, $units) {
c5da9906 1263 // Make units more useful
1264 $tmpunits = array();
1265 foreach ($units as $unit) {
1266 $tmpunits[$unit->unit] = $unit->multiplier;
1267 }
1268 // remove spaces and normalise decimal places.
1269 $search = array(' ', ',');
1270 $replace = array('', '.');
1271 $rawresponse = str_replace($search, $replace, trim($rawresponse));
1272
1273 // Apply any unit that is present.
1274 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
1275 $rawresponse, $responseparts)) {
1276
1277 if (!empty($responseparts[5])) {
1278
1279 if (isset($tmpunits[$responseparts[5]])) {
1280 // Valid number with unit.
1281 return true ; //(float)$responseparts[1] / $tmpunits[$responseparts[5]];
1fe641f7 1282 } else {
1283 // Valid number with invalid unit. Must be wrong.
1284 return false;
1285 }
1286
516cf3eb 1287 } else {
1fe641f7 1288 // Valid number without unit.
c5da9906 1289 return false ; //(float)$responseparts[1];
516cf3eb 1290 }
1291 }
1fe641f7 1292 // Invalid number. Must be wrong.
1293 return false;
516cf3eb 1294 }
f34488b2 1295
b9bd6da4 1296 /**
1297 * Runs all the code required to set up and save an essay question for testing purposes.
1298 * Alternate DB table prefix may be used to facilitate data deletion.
1299 */
1300 function generate_test($name, $courseid = null) {
1301 global $DB;
1302 list($form, $question) = default_questiontype::generate_test($name, $courseid);
1303 $question->category = $form->category;
1304
1305 $form->questiontext = "What is 674 * 36?";
1306 $form->generalfeedback = "Thank you";
1307 $form->penalty = 0.1;
1308 $form->defaultgrade = 1;
1309 $form->noanswers = 3;
1310 $form->answer = array('24264', '24264', '1');
1311 $form->tolerance = array(10, 100, 0);
1312 $form->fraction = array(1, 0.5, 0);
1313 $form->nounits = 2;
1314 $form->unit = array(0 => null, 1 => null);
1315 $form->multiplier = array(1, 0);
1316 $form->feedback = array('Very good', 'Close, but not quite there', 'Well at least you tried....');
1317
1318 if ($courseid) {
1319 $course = $DB->get_record('course', array('id' => $courseid));
1320 }
1321
1322 return $this->save_question($question, $form, $course);
1323 }
fe6ce234 1324
5d548d3e 1325 function move_files($questionid, $oldcontextid, $newcontextid) {
fe6ce234 1326 $fs = get_file_storage();
5d548d3e
TH
1327
1328 parent::move_files($questionid, $oldcontextid, $newcontextid);
1329 $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid);
1330
1331 $fs->move_area_files_to_new_context($oldcontextid,
1332 $newcontextid, 'qtype_numerical', 'instruction', $questionid);
fe6ce234
DC
1333 }
1334
9203b705
TH
1335 protected function delete_files($questionid, $contextid) {
1336 $fs = get_file_storage();
1337
1338 parent::delete_files($questionid, $contextid);
1339 $this->delete_files_in_answers($questionid, $contextid);
1340 $fs->delete_area_files($contextid, 'qtype_numerical', 'instruction', $questionid);
1341 }
1342
fe6ce234
DC
1343 function check_file_access($question, $state, $options, $contextid, $component,
1344 $filearea, $args) {
1345 $itemid = reset($args);
1346 if ($component == 'question' && $filearea == 'answerfeedback') {
1347 $result = $options->feedback && array_key_exists($itemid, $question->options->answers);
1348 if (!$result) {
1349 return false;
1350 }
1351 foreach($question->options->answers as $answer) {
1352 if ($this->test_response($question, $state, $answer)) {
1353 return true;
1354 }
1355 }
1356 return false;
1357 } else if ($filearea == 'instruction') {
1358 if ($itemid != $question->id) {
1359 return false;
1360 } else {
1361 return true;
1362 }
1363 } else {
1364 return parent::check_file_access($question, $state, $options, $contextid, $component,
1365 $filearea, $args);
1366 }
1367 }
516cf3eb 1368}
516cf3eb 1369
1fe641f7 1370// INITIATION - Without this line the question type is not in use.
a2156789 1371question_register_questiontype(new question_numerical_qtype());
04e91671
PP
1372if ( ! defined ("NUMERICALQUESTIONUNITTEXTINPUTDISPLAY")) {
1373 define("NUMERICALQUESTIONUNITTEXTINPUTDISPLAY", 0);
1374}
1375if ( ! defined ("NUMERICALQUESTIONUNITMULTICHOICEDISPLAY")) {
1376 define("NUMERICALQUESTIONUNITMULTICHOICEDISPLAY", 1);
1377}
1378if ( ! defined ("NUMERICALQUESTIONUNITTEXTDISPLAY")) {
1379 define("NUMERICALQUESTIONUNITTEXTDISPLAY", 2);
1380}
1381if ( ! defined ("NUMERICALQUESTIONUNITNODISPLAY")) {
1382 define("NUMERICALQUESTIONUNITNODISPLAY", 3);
1383}