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