MDL-25104 removing unnecessary $options = array_shift($oldoptions) line 192 that...
[moodle.git] / question / type / numerical / questiontype.php
1 <?php
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/>.
18 /**
19  * @author Martin Dougiamas and many others. Tim Hunt.
20  * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
21  * @package questionbank
22  * @subpackage questiontypes
23  */
25 require_once("$CFG->dirroot/question/type/shortanswer/questiontype.php");
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...
35  * @package questionbank
36  * @subpackage questiontypes
37  */
39 class question_numerical_qtype extends question_shortanswer_qtype {
41     public $virtualqtype = false;
42     function name() {
43         return 'numerical';
44     }
46     function has_wildcards_in_responses() {
47         return true;
48     }
50     function requires_qtypes() {
51         return array('shortanswer');
52     }
54     function get_question_options(&$question) {
55         // Get the question answers and their respective tolerances
56         // Note: question_numerical is an extension of the answer table rather than
57         //       the question table as is usually the case for qtype
58         //       specific tables.
59         global $CFG, $DB, $OUTPUT;
60         if (!$question->options->answers = $DB->get_records_sql(
61                                 "SELECT a.*, n.tolerance " .
62                                 "FROM {question_answers} a, " .
63                                 "     {question_numerical} n " .
64                                 "WHERE a.question = ? " .
65                                 "    AND   a.id = n.answer " .
66                                 "ORDER BY a.id ASC", array($question->id))) {
67             echo $OUTPUT->notification('Error: Missing question answer for numerical question ' . $question->id . '!');
68             return false;
69         }
70         $this->get_numerical_units($question);
71         //get_numerical_options() need to know if there are units
72         // to set correctly default values
73         $this->get_numerical_options($question);
75         // If units are defined we strip off the default unit from the answer, if
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);
81                 if ($length && substr($answer, -$length) == $defaultunit->unit) {
82                     $question->options->answers[$key]->answer =
83                             substr($answer, 0, strlen($answer)-$length);
84                 }
85             }
86         }
88         return true;
89     }
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     }
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     }
115     function get_numerical_options(&$question) {
116         global $DB;
117         if (!$options = $DB->get_record('question_numerical_options', array('question' => $question->id))) {
118             $question->options->unitgradingtype = 0; // total grade
119             $question->options->unitpenalty = 0.1; // default for old questions 
120             // the default
121             if ($defaultunit = $this->get_default_numerical_unit($question)) {
122                 // so units can be graded
123                 $question->options->showunits = NUMERICALQUESTIONUNITTEXTINPUTDISPLAY ;
124             }else {
125                 // only numerical will be graded
126                 $question->options->showunits = NUMERICALQUESTIONUNITNODISPLAY ;
127             }
128             $question->options->unitsleft = 0 ;
129             $question->options->instructions = '';
130             $question->options->instructionsformat = editors_get_preferred_format();
131         } else {
132             $question->options->unitgradingtype = $options->unitgradingtype;
133             $question->options->unitpenalty = $options->unitpenalty;
134             $question->options->showunits = $options->showunits;
135             $question->options->unitsleft = $options->unitsleft;
136             $question->options->instructions = $options->instructions;
137             $question->options->instructionsformat = $options->instructionsformat;
138         }
140         return true;
141     }
144     /**
145      * Save the units and the answers associated with this question.
146      */
147     function save_question_options($question) {
148         global $DB;
149         $context = $question->context;
151         // Get old versions of the objects
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');
157         // Save the units.
158         $result = $this->save_numerical_units($question);
159         if (isset($result->error)) {
160             return $result;
161         } else {
162             $units = $result->units;
163         }
165         // Insert all the new answers
166         foreach ($question->answer as $key => $answerdata) {
167             // Check for, and ingore, completely blank answer from the form.
168             if (trim($answerdata) == '' && $question->fraction[$key] == 0 &&
169                     html_is_blank($question->feedback[$key]['text'])) {
170                 continue;
171             }
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             }
183             if (trim($answerdata) === '*') {
184                 $answer->answer = '*';
185             } else {
186                 $answer->answer = $this->apply_unit($answerdata, $units);
187                 if ($answer->answer === false) {
188                     $result->notice = get_string('invalidnumericanswer', 'quiz');
189                 }
190             }
191             $answer->fraction = $question->fraction[$key];
192             $answer->feedback = $this->import_or_save_files($question->feedback[$key],
193                     $context, 'question', 'answerfeedback', $answer->id);
194             $answer->feedbackformat = $question->feedback[$key]['format'];
195             $DB->update_record('question_answers', $answer);
197             // Set up the options object
198             if (!$options = array_shift($oldoptions)) {
199                 $options = new stdClass();
200             }
201             $options->question = $question->id;
202             $options->answer   = $answer->id;
203             if (trim($question->tolerance[$key]) == '') {
204                 $options->tolerance = '';
205             } else {
206                 $options->tolerance = $this->apply_unit($question->tolerance[$key], $units);
207                 if ($options->tolerance === false) {
208                     $result->notice = get_string('invalidnumerictolerance', 'quiz');
209                 }
210             }
211             if (isset($options->id)) {
212                 $DB->update_record('question_numerical', $options);
213             } else {
214                 $DB->insert_record('question_numerical', $options);
215             }
216         }
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));
223         }
224         foreach($oldoptions as $oldoption) {
225             $DB->delete_records('question_numerical', array('id' => $oldoption->id));
226         }
228         $result = $this->save_numerical_options($question);
229         if (!empty($result->error) || !empty($result->notice)) {
230             return $result;
231         }
233         return true;
234     }
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      */
244     function save_numerical_options($question) {
245         global $DB;
247         $result = new stdClass;
249         $update = true ;
250         $options = $DB->get_record('question_numerical_options', array('question' => $question->id));
251         if (!$options) {
252             $options = new stdClass;
253             $options->question = $question->id;
254             $options->instructions = '';
255             $options->id = $DB->insert_record('question_numerical_options', $options);
256         }
258         if (isset($question->options->unitgradingtype)) {
259             $options->unitgradingtype = $question->options->unitgradingtype;
260         } else {
261             $options->unitgradingtype = 0 ;
262         }
263         if (isset($question->unitpenalty)){
264             $options->unitpenalty = $question->unitpenalty;
265         } else { //so this is either an old question or a close question type
266             $options->unitpenalty = 1 ;
267         }
268         // if we came from the form then 'unitrole' exists
269         if (isset($question->unitrole)){
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 ;
281             }
282         } else {
283             if (isset($question->showunits)){
284                 $options->showunits = $question->showunits;
285             } else {
286                 if ($defaultunit = $this->get_default_numerical_unit($question)) {
287                     // so units can be used
288                     $options->showunits = NUMERICALQUESTIONUNITTEXTINPUTDISPLAY ;
289                 } else {
290                     // only numerical will be graded
291                     $options->showunits = NUMERICALQUESTIONUNITNODISPLAY ;
292                 }
293             }
294         }
296         if (isset($question->unitsleft)) {
297             $options->unitsleft = $question->unitsleft;
298         } else {
299             $options->unitsleft = 0 ;
300         }
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'];
306         $DB->update_record('question_numerical_options', $options);
308         return $result;
309     }
311     function save_numerical_units($question) {
312         global $DB;
313         $result = new stdClass;
315         // Delete the units previously saved for this question.
316         $DB->delete_records('question_numerical_units', array('question' => $question->id));
318         // Nothing to do.
319         if (!isset($question->multiplier)) {
320             $result->units = array();
321             return $result;
322         }
324         // Save the new units.
325         $units = array();
326         $unitalreadyinsert = array();
327         foreach ($question->multiplier as $i => $multiplier) {
328             // Discard any unit which doesn't specify the unit or the multiplier
329             if (!empty($question->multiplier[$i]) && !empty($question->unit[$i])&& !array_key_exists($question->unit[$i],$unitalreadyinsert)) {
330                 $unitalreadyinsert[$question->unit[$i]] = 1 ;
331                 $units[$i] = new stdClass;
332                 $units[$i]->question = $question->id;
333                 $units[$i]->multiplier = $this->apply_unit($question->multiplier[$i], array());
334                 $units[$i]->unit = $question->unit[$i];
335                 $DB->insert_record('question_numerical_units', $units[$i]);
336             }
337         }
338         unset($question->multiplier, $question->unit);
340         $result->units = &$units;
341         return $result;
342     }
344     function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
345         $state->responses = array();
346         $state->responses['answer'] =  '';
347         $state->responses['unit'] = '';
349         return true;
350     }
351     function restore_session_and_responses(&$question, &$state) {
352        if(false === strpos($state->responses[''], '|||||')){
353              $state->responses['answer']= $state->responses[''];
354              $state->responses['unit'] = '';
355              $this->split_old_answer($state->responses[''], $question->options->units, $state->responses['answer'] ,$state->responses['unit'] );
356        }else {
357             $responses = explode('|||||', $state->responses['']);
358             $state->responses['answer']= $responses[0];
359             $state->responses['unit'] = $responses[1];
360        }
362        return true;
363     }
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                 }
372             }
373         return 0 ;
374     }
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)) {
384             if(isset($responseparts[5]) ){
385                 $unit = $responseparts[5] ;
386             }
387             if(isset($responseparts[1]) ){
388                 $answer = $responseparts[1] ;
389             }
390         }
391         return ;
392     }
395     function save_session_and_responses(&$question, &$state) {
396         global $DB;
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
407         $DB->set_field('question_states', 'answer', $responses, array('id' => $state->id));
408         return true;
409     }
411     function delete_question($questionid, $contextid) {
412         global $DB;
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));
417         parent::delete_question($questionid, $contextid);
418     }
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) {
425         global $CFG, $OUTPUT;
427         $context = $this->get_context_by_category_id($question->category);
428         $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
429         $formatoptions = new stdClass;
430         $formatoptions->noclean = true;
431         $formatoptions->para = false;
432         $nameprefix = $question->name_prefix;
433         $component = 'qtype_' . $question->qtype;
434         // rewrite instructions text
435         $question->options->instructions = quiz_rewrite_question_urls($question->options->instructions, 'pluginfile.php', $context->id, $component, 'instructions', array($state->attempt, $state->question), $question->id);
437         /// Print question text and media
439         $questiontext = format_text($question->questiontext,
440                 $question->questiontextformat,
441                 $formatoptions, $cmoptions->course);
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
446         // If $question->options->showunits == NUMERICALQUESTIONUNITTEXTDISPLAY
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.
452         $nameanswer = "name=\"".$question->name_prefix."answer\"";
453         $nameunit   = "name=\"".$question->name_prefix."unit\"";
454         // put old answer data in $state->responses['answer'] and $state->responses['unit']
455         if (isset($state->responses['']) && $state->responses[''] != '' && !isset($state->responses['answer'])){
456               $this->split_old_answer($state->responses[''], $question->options->units, $state->responses['answer'] ,$state->responses['unit'] );
457         }
458         // prepare the values of the input elements to be dispalyed answer i.e. number  and unit 
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']).'" ';
466         } else {
467             $valueunit = ' value="" ';
468             if ($question->options->showunits == NUMERICALQUESTIONUNITTEXTDISPLAY ){
469               $valueunit = ' value="'.s($question->options->units[0]->unit).'" ';
470             }
471         }
473         $feedback = '';
474         $class = '';
475         $classunit = '' ;
476         $classunitvalue = '' ;
477         $feedbackimg = '';
478         $feedbackimgunit = '' ;
479         $answerasterisk = false ;
480         $response = '' ;
481         $valid_numerical_unit = false ;
482         $valid_numerical_unit_index = -1 ;
483         $unit_in_numerical_answer = false ;
484         $rawgrade = 0 ;
485         if ($options->feedback) {
486             $class = question_get_feedback_class(0);
487             $classunit = question_get_feedback_class(0);
488             $feedbackimg = question_get_feedback_image(0);
489             $feedbackimgunit = question_get_feedback_image(0);
490             $classunitvalue = 0 ;
491             $valid_numerical_unit_index = -1 ;
492             // if there is unit in answer and unitgradingtype = 0 
493             // the grade is 0            
494             //this is OK for the first answer with a good response
495             // having to test for * so response as long as not empty
496            // $response = $this->extract_numerical_response($state->responses['answer']);
497             // test for a greater than 0 grade    
498             foreach($question->options->answers as $answer) {
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
505                     $class = question_get_feedback_class($answer->fraction);
506                     $feedbackimg = question_get_feedback_image($answer->fraction);
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 ;
517                         
518                     }else {
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                         */ 
534                         $valid_numerical_unit = false ;
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 * 
542                         } else { 
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 ;
550                             }else {
551                                 $unit_in_numerical_answer = false ;
552                             }
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 ;
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 ;
583                                                 $valid_numerical_unit_index = $key ;
584                                                 break ;
585                                             }
586                                         }
587                                     }
588                                 }
589                             }
590                         }
591                     }
592                     if ($answer->feedback) {
593                         $answer->feedback = quiz_rewrite_question_urls($answer->feedback, 'pluginfile.php', $context->id, 'question', 'answerfeedback', array($state->attempt, $state->question), $answer->id);
594                         $feedback = format_text($answer->feedback, $answer->feedbackformat, $formatoptions, $cmoptions->course);
595                     }
596                     
597                     break;
598                 }
599             }
600     }
601         $state->options->raw_unitpenalty = 0 ;
602         $raw_unitpenalty = 0 ;
603         if( $question->options->showunits == NUMERICALQUESTIONUNITNODISPLAY ||
604                 $question->options->showunits == NUMERICALQUESTIONUNITTEXTDISPLAY ) {
605                     $classunitvalue = 1 ;
606         }
608         if(! $answerasterisk  && $question->options->unitgradingtype != 0 && (! $valid_numerical_unit || $unit_in_numerical_answer)){
609             if($question->options->unitgradingtype == 1){
610                 $raw_unitpenalty = $question->options->unitpenalty * $rawgrade ;
611             }else {
612                 $raw_unitpenalty = $question->options->unitpenalty * $question->maxgrade;
613             }
614             $state->options->raw_unitpenalty = $raw_unitpenalty ;
615         }
617         /// Removed correct answer, to be displayed later MDL-7496
618         include("$CFG->dirroot/question/type/numerical/display.html");
619     }
622     function compare_responses(&$question, $state, $teststate) {
624                if ($question->options->showunits == NUMERICALQUESTIONUNITMULTICHOICEDISPLAY && isset($question->options->units) && isset($state->responses['unit']) && isset($question->options->units[$state->responses['unit']] )){
625             $state->responses['unit']=$question->options->units[$state->responses['unit']]->unit;
626         };
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         }
644         if ( isset($responses)  && isset($testresponses )) {
646             return $responses == $testresponses ;
647         }
648         return false;
649     }
651     /**
652      * Checks whether a response matches a given answer, taking the tolerance
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.
656      * if unit != -1 then the test is done only on this unit
657      */
658     function test_response(&$question, &$state, $answer ) {
659         // Deal with the match anything answer.
660         if ($answer->answer === '*') {
661             return true;
662         }
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[''];
669             }
670             $answertotest = $state->responses['answer'];
671             // values coming from  NUMERICALQUESTIONUNITTEXTINPUTDISPLAY
672             // or NUMERICALQUESTIONUNITTEXTDISPLAY as unit hidden HTML element
673             
674             if($question->options->showunits == NUMERICALQUESTIONUNITTEXTINPUTDISPLAY ){
675                 
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             }
682             if(isset($state->responses['unit'])) {
683                 $answertotest .= $state->responses['unit'] ;    
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);
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);
697             return ($answer->min <= $response && $response <= $answer->max);
698         } else { // $question->options->unitgradingtype > 0 
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             
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){
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;
740         }
741         return false;
742     }
744     /**
745     * Performs response processing and grading
746     * The function was redefined for handling correctly the two parts
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.
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     */
767     function grade_responses(&$question, &$state, $cmoptions) {
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'] );
770         }
772         $state->raw_grade = 0;
773         $valid_numerical_unit = false ;
774         $break = 0 ;
775         $unittested = '';
776         $hasunits = 0 ;
777         $answerasterisk = false ;
779         $break = 0 ;
780         foreach($question->options->answers as $answer) {
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                         }
811                     }
812                 }
813                 break ;
814             } 
815         }
816         // apply unit penalty
817         $raw_unitpenalty = 0 ;
818         if($question->options->unitgradingtype != 0 && !empty($question->options->unitpenalty)&& $valid_numerical_unit != true ){
819             if($question->options->unitgradingtype == 1){
820                 $raw_unitpenalty = $question->options->unitpenalty * $state->raw_grade ;
821             }else {
822                 $raw_unitpenalty = $question->options->unitpenalty * $question->maxgrade;
823             }
824             $state->raw_grade -= $raw_unitpenalty ;
825         }
826         
827         // Make sure we don't assign negative or too high marks.
828         $state->raw_grade = min(max((float) $state->raw_grade,
829                             0.0), 1.0) * $question->maxgrade;
830         
831         // Update the penalty.
832         $state->penalty = $question->penalty * $question->maxgrade;
834         // mark the state as graded
835         $state->event = ($state->event ==  QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
837         return true;
838     }
841     function get_correct_responses(&$question, &$state) {
842         $correct = parent::get_correct_responses($question, $state);
843         $unit = $this->get_default_numerical_unit($question);
844         if (isset($correct['']) && $correct[''] != '*' && $unit) {
845             $correct[''] .= ' '.$unit->unit;
846         }
847         return $correct;
848     }
850     // ULPGC ecastro
851     function get_all_responses(&$question, &$state) {
852         $result = new stdClass;
853         $answers = array();
854         $unit = $this->get_default_numerical_unit($question);
855         if (is_array($question->options->answers)) {
856             foreach ($question->options->answers as $aid=>$answer) {
857                 $r = new stdClass;
858                 $r->answer = $answer->answer;
859                 $r->credit = $answer->fraction;
860                 $this->get_tolerance_interval($answer);
861                 if ($r->answer != '*' && $unit) {
862                     $r->answer .= ' ' . $unit->unit;
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             }
871         }
872         $result->id = $question->id;
873         $result->responses = $answers;
874         return $result;
875     }
876     function get_actual_response($question, $state) {
877        if (!empty($state->responses) && !empty($state->responses[''])) {
878            if(false === strpos($state->responses[''], '|||||')){
879                 $responses[] = $state->responses[''];
880             }else {
881                 $resp = explode('|||||', $state->responses['']);
882                 $responses[] = $resp[0].$resp[1];
883             }
884        } else {
885            $responses[] = '';
886         }
888        return $responses;
889     }
892     function get_tolerance_interval(&$answer) {
893         // No tolerance
894         if (empty($answer->tolerance)) {
895             $answer->tolerance = 0;
896         }
898         // Calculate the interval of correct responses (min/max)
899         if (!isset($answer->tolerancetype)) {
900             $answer->tolerancetype = 2; // nominal
901         }
903         // We need to add a tiny fraction depending on the set precision to make the
904         // comparison work correctly. Otherwise seemingly equal values can yield
905         // false. (fixes bug #3225)
906         $tolerance = (float)$answer->tolerance + ("1.0e-".ini_get('precision'));
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;
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;
917             case '2': case 'nominal':
918                 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
919                 // $answer->tolerance 0 or something else
920                 if ((float)$answer->tolerance == 0.0  &&  abs((float)$answer->answer) <= $tolerance ){
921                     $tolerance = (float) ("1.0e-".ini_get('precision')) * abs((float)$answer->answer) ; //tiny fraction
922                 } else if ((float)$answer->tolerance != 0.0 && abs((float)$answer->tolerance) < abs((float)$answer->answer) &&  abs((float)$answer->answer) <= $tolerance){
923                     $tolerance = (1+("1.0e-".ini_get('precision')) )* abs((float) $answer->tolerance) ;//tiny fraction
924                }
926                 $max = $answer->answer + $tolerance;
927                 $min = $answer->answer - $tolerance;
928                 break;
929             case '3': case 'geometric':
930                 $quotient = 1 + abs($tolerance);
931                 $max = $answer->answer * $quotient;
932                 $min = $answer->answer / $quotient;
933                 break;
934             default:
935                 print_error('unknowntolerance', 'question', '', $answer->tolerancetype);
936         }
938         $answer->min = $min;
939         $answer->max = $max;
940         return true;
941     }
943     /**
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      */
952     function extract_numerical_response($rawresponse) {
953         $extractedresponse = new stdClass() ;
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('', '');
959         }else { // remove spaces and normalise , to a . .
960             $replace = array('', '.');
961         }
962         $rawresponse = str_replace($search, $replace, $rawresponse);
964          if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
965                 $rawresponse, $responseparts)) {
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 = '';
975         }
976                 
977         // Invalid number. Must be wrong.
978         return clone($extractedresponse) ;
979     }
980     /**
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      */
989     function apply_unit($rawresponse, $units) {
991         // Make units more useful
992         $tmpunits = array();
993         foreach ($units as $unit) {
994             $tmpunits[$unit->unit] = $unit->multiplier;
995         }
996         // remove spaces and normalise decimal places.
997         $rawresponse = trim($rawresponse) ;
998         $search  = array(' ', ',');
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('', '');
1002         }else { // remove spaces and normalise , to a . .
1003             $replace = array('', '.');
1004         }
1005         $rawresponse = str_replace($search, $replace, $rawresponse);
1008         // Apply any unit that is present.
1009         if (ereg('^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$',
1010                 $rawresponse, $responseparts)) {
1011            //     echo"<p> responseparts <pre>";print_r($responseparts) ;echo"</pre></p>";
1013             if (!empty($responseparts[5])) {
1015                 if (isset($tmpunits[$responseparts[5]])) {
1016                     // Valid number with unit.
1017                     return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
1018                 } else {
1019                     // Valid number with invalid unit. Must be wrong.
1020                     return false;
1021                 }
1023             } else {
1024                 // Valid number without unit.
1025                 return (float)$responseparts[1];
1026             }
1027         }
1028         // Invalid number. Must be wrong.
1029         return false;
1030     }
1032     /**
1033      * function used in function definition_inner()
1034      * of edit_..._form.php for
1035      * numerical, calculated, calculatedsimple
1036      */
1037     function add_units_options(&$mform, &$that){
1038         // Units are graded
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);
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);
1043         $mform->addElement('static', 'separator2', '', '<HR/>');
1044         $mform->addElement('radio', 'unitrole', get_string('unitgraded1', 'qtype_numerical'), get_string('unitgraded', 'qtype_numerical'),3);
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'));
1049         $penaltygrp[] =& $mform->createElement('select', 'unitgradingtypes', '' , $unitgradingtypes );
1050         $mform->addGroup($penaltygrp, 'penaltygrp', get_string('unitpenalty', 'qtype_numerical'),' ' , false);
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);
1054         $mform->addGroup($multichoicedisplaygrp, 'multichoicedisplaygrp', get_string('studentunitanswer', 'qtype_numerical'),' OR ' , false);        
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 );
1058         $mform->addElement('static', 'separator2', '<HR/>', '<HR/>');
1060         $mform->addElement('editor', 'instructions', get_string('instructions', 'qtype_numerical'), null, $that->editoroptions);
1061         $showunits1grp = array();
1062         $mform->addElement('static', 'separator2', '<HR/>', '<HR/>');
1064         $mform->setType('unitpenalty', PARAM_NUMBER);
1065         $mform->setDefault('unitpenalty', 0.1);
1066         $mform->setDefault('unitgradingtypes', 1);
1067         $mform->addHelpButton('penaltygrp', 'unitpenalty', 'qtype_numerical'); // TODO help did not exist before MDL-21695
1068         $mform->setDefault('unitsleft', 0);
1069         $mform->setType('instructions', PARAM_RAW);
1070         $mform->addHelpButton('instructions', 'numericalinstructions', 'qtype_numerical');
1071         $mform->disabledIf('penaltygrp', 'unitrole','eq','0');
1072         $mform->disabledIf('penaltygrp', 'unitrole','eq','1');
1073         $mform->disabledIf('penaltygrp', 'unitrole','eq','2');
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');
1078     }
1080     /**
1081      * function used in in function definition_inner()
1082      * of edit_..._form.php for
1083      * numerical, calculated, calculatedsimple
1084      */
1085     function add_units_elements(& $mform,& $that) {
1086         $repeated = array();
1087         $repeated[] =& $mform->createElement('header', 'unithdr', get_string('unithdr', 'qtype_numerical', '{no}'));
1089         $repeated[] =& $mform->createElement('text', 'unit', get_string('unit', 'quiz'));
1090         $mform->setType('unit', PARAM_NOTAGS);
1092         $repeated[] =& $mform->createElement('text', 'multiplier', get_string('multiplier', 'quiz'));
1093         $mform->setType('multiplier', PARAM_NUMBER);
1095         if (isset($that->question->options)){
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}'));
1107         if ($mform->elementExists('multiplier[0]')){
1108             $firstunit =& $mform->getElement('multiplier[0]');
1109             $firstunit->freeze();
1110             $firstunit->setValue('1.0');
1111             $firstunit->setPersistantFreeze(true);
1112             $mform->addHelpButton('multiplier[0]', 'numericalmultiplier', 'qtype_numerical');
1113         }
1114     }
1116     /**
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){
1122         list($categoryid) = explode(',', $question->category);
1123         $context = $this->get_context_by_category_id($categoryid);
1125         if (isset($question->options)){
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             } 
1133             $default_values['unitpenalty'] = $question->options->unitpenalty ;
1134             switch ($question->options->showunits){
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 ;
1149                     break;
1150                 case 2 : // NUMERICALQUESTIONUNITTEXTDISPLAY
1151                     $default_values['unitrole'] = 1 ;
1152                 case 3 : // NUMERICALQUESTIONUNITNODISPLAY
1153                     $default_values['unitrole'] = 0 ;
1154                   //  $default_values['showunits1'] = $question->options->showunits ;
1155                     break;
1156             }
1157             $default_values['unitsleft'] = $question->options->unitsleft ;
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;
1175             if (isset($question->options->units)) {
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     }
1187     /**
1188       * function use in in function validation()
1189       * of edit_..._form.php for
1190       * numerical, calculated, calculatedsimple
1191       */
1193     function validate_numerical_options(& $data, & $errors){
1194         $units  = $data['unit'];
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             }
1206         if (($showunits == NUMERICALQUESTIONUNITTEXTINPUTDISPLAY) ||
1207                 ($showunits == NUMERICALQUESTIONUNITMULTICHOICEDISPLAY ) ||
1208                 ($showunits == NUMERICALQUESTIONUNITTEXTDISPLAY )){
1209            if (trim($units[0]) == ''){
1210              $errors['unit[0]'] = 'You must set a valid unit name' ;
1211             }
1212         }
1213         if ($showunits == NUMERICALQUESTIONUNITNODISPLAY ){
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         }
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                         }
1255                     }
1256                 }
1257             }
1259     }
1260  
1262     function valid_unit($rawresponse, $units) {
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));
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)) {
1277             if (!empty($responseparts[5])) {
1279                 if (isset($tmpunits[$responseparts[5]])) {
1280                     // Valid number with unit.
1281                     return true ; //(float)$responseparts[1] / $tmpunits[$responseparts[5]];
1282                 } else {
1283                     // Valid number with invalid unit. Must be wrong.
1284                     return false;
1285                 }
1287             } else {
1288                 // Valid number without unit.
1289                 return false ; //(float)$responseparts[1];
1290             }
1291         }
1292         // Invalid number. Must be wrong.
1293         return false;
1294     }
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;
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....');
1318         if ($courseid) {
1319             $course = $DB->get_record('course', array('id' => $courseid));
1320         }
1322         return $this->save_question($question, $form, $course);
1323     }
1325     function move_files($questionid, $oldcontextid, $newcontextid) {
1326         $fs = get_file_storage();
1328         parent::move_files($questionid, $oldcontextid, $newcontextid);
1329         $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid);
1331         $fs->move_area_files_to_new_context($oldcontextid,
1332                 $newcontextid, 'qtype_numerical', 'instruction', $questionid);
1333     }
1335     protected function delete_files($questionid, $contextid) {
1336         $fs = get_file_storage();
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     }
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     }
1370 // INITIATION - Without this line the question type is not in use.
1371 question_register_questiontype(new question_numerical_qtype());
1372 if ( ! defined ("NUMERICALQUESTIONUNITTEXTINPUTDISPLAY")) {
1373     define("NUMERICALQUESTIONUNITTEXTINPUTDISPLAY",   0);
1375 if ( ! defined ("NUMERICALQUESTIONUNITMULTICHOICEDISPLAY")) {
1376     define("NUMERICALQUESTIONUNITMULTICHOICEDISPLAY",   1);
1378 if ( ! defined ("NUMERICALQUESTIONUNITTEXTDISPLAY")) {
1379     define("NUMERICALQUESTIONUNITTEXTDISPLAY",   2);
1381 if ( ! defined ("NUMERICALQUESTIONUNITNODISPLAY")) {
1382     define("NUMERICALQUESTIONUNITNODISPLAY",    3);