MDL-10110 MDL-20296 improve grading when unit input is empty
[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         if (!$oldanswers = $DB->get_records('question_answers', array('question' =>  $question->id), 'id ASC')) {
153             $oldanswers = array();
154         }
156         if (!$oldoptions = $DB->get_records('question_numerical', array('question' =>  $question->id), 'answer ASC')) {
157             $oldoptions = array();
158         }
160         // Save the units.
161         $result = $this->save_numerical_units($question);
162         if (isset($result->error)) {
163             return $result;
164         } else {
165             $units = &$result->units;
166         }
168         // Insert all the new answers
169         foreach ($question->answer as $key => $dataanswer) {
170             // Check for, and ingore, completely blank answer from the form.
171             if (trim($dataanswer) == '' && $question->fraction[$key] == 0 &&
172                     html_is_blank($question->feedback[$key]['text'])) {
173                 continue;
174             }
176             $answer = new stdClass;
177             $answer->question = $question->id;
178             if (trim($dataanswer) === '*') {
179                 $answer->answer = '*';
180             } else {
181                 $answer->answer = $this->apply_unit($dataanswer, $units);
182                 if ($answer->answer === false) {
183                     $result->notice = get_string('invalidnumericanswer', 'quiz');
184                 }
185             }
186             $answer->fraction = $question->fraction[$key];
188             $feedbacktext = trim($question->feedback[$key]['text']);
189             $draftid = $question->feedback[$key]['itemid'];
192             $answer->feedbackformat = $question->feedback[$key]['format'];
194             if ($oldanswer = array_shift($oldanswers)) {  // Existing answer, so reuse it
195                 $feedbacktext = file_save_draft_area_files($draftid, $context->id, 'question', 'answerfeedback', $oldanswer->id, self::$fileoptions, $feedbacktext);
196                 $answer->feedback = $feedbacktext;
197                 $answer->id = $oldanswer->id;
198                 $DB->update_record("question_answers", $answer);
199             } else { // This is a completely new answer
200                 $answer->feedback = $feedbacktext;
201                 $answer->id = $DB->insert_record("question_answers", $answer);
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));
204             }
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 {
215                 $options->tolerance = $this->apply_unit($question->tolerance[$key], $units);
216                 if ($options->tolerance === false) {
217                     $result->notice = get_string('invalidnumerictolerance', 'quiz');
218                 }
219             }
221             // Save options
222             if (isset($options->id)) { // reusing existing record
223                 $DB->update_record('question_numerical', $options);
224             } else { // new options
225                 $DB->insert_record('question_numerical', $options);
226             }
227         }
228         // delete old answer records
229         if (!empty($oldanswers)) {
230             foreach($oldanswers as $oa) {
231                 $DB->delete_records('question_answers', array('id' => $oa->id));
232             }
233         }
235         // delete old answer records
236         if (!empty($oldoptions)) {
237             foreach($oldoptions as $oo) {
238                 $DB->delete_records('question_numerical', array('id' => $oo->id));
239             }
240         }
241         $result = $this->save_numerical_options($question);
242         if (isset($result->error)) {
243             return $result;
244         }
245         // Report any problems.
246         if (!empty($result->notice)) {
247             return $result;
248         }
249         return true;
250     }
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      */
260     function save_numerical_options(&$question) {
261         global $DB;
262         //        echo"<p> ".$question->id."question<pre>";print_r($question) ;echo"</pre></p>";
264         $result = new stdClass;
265         // numerical options
266         $update = true ;
267         $options = $DB->get_record('question_numerical_options', array('question' => $question->id));
268         if (!$options) {
269             $update = false;
270             $options = new stdClass;
271             $options->question = $question->id;
272         }
273         if(isset($question->options->unitgradingtype)){
274             $options->unitgradingtype = $question->options->unitgradingtype;
275         }else {
276             $options->unitgradingtype = 0 ;
277         }
278         if(isset($question->unitpenalty)){
279             $options->unitpenalty = $question->unitpenalty;
280         }else { //so this is either an old question or a close question type
281             $options->unitpenalty = 1 ;
282         }
283         // if we came from the form then 'unitrole' exists
284         if(isset($question->unitrole)){
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 ;
296             }
297         } else {
298             if(isset($question->showunits)){
299                 $options->showunits = $question->showunits;
300             }else {
301                 if ($defaultunit = $this->get_default_numerical_unit($question)) {
302                     // so units can be used
303                     $options->showunits = NUMERICALQUESTIONUNITTEXTINPUTDISPLAY ;
304                 }else {
305                     // only numerical will be graded
306                     $options->showunits = NUMERICALQUESTIONUNITNODISPLAY ;
307                 }
308             }
309         }
310         if(isset($question->unitsleft)){
311             $options->unitsleft = $question->unitsleft;
312         }else {
313             $options->unitsleft = 0 ;
314         }
315         $options->instructionsformat = $question->instructions['format'];
316         if(isset($question->instructions)){
317             $options->instructions = trim($question->instructions['text']);
318         }else {
319             $options->instructions = '' ;
320         }
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         );
330         if ($update) {
331             $DB->update_record("question_numerical_options", $options);
332         } else {
333             $id = $DB->insert_record("question_numerical_options", $options);
334         }
336         return $result;
337     }
339     function save_numerical_units($question) {
340         global $DB;
341         $result = new stdClass;
343         // Delete the units previously saved for this question.
344         $DB->delete_records('question_numerical_units', array('question' => $question->id));
346         // Nothing to do.
347         if (!isset($question->multiplier)) {
348             $result->units = array();
349             return $result;
350         }
352         // Save the new units.
353         $units = array();
354         $unitalreadyinsert = array();
355         foreach ($question->multiplier as $i => $multiplier) {
356             // Discard any unit which doesn't specify the unit or the multiplier
357             if (!empty($question->multiplier[$i]) && !empty($question->unit[$i])&& !array_key_exists($question->unit[$i],$unitalreadyinsert)) {
358                 $unitalreadyinsert[$question->unit[$i]] = 1 ;
359                 $units[$i] = new stdClass;
360                 $units[$i]->question = $question->id;
361                 $units[$i]->multiplier = $this->apply_unit($question->multiplier[$i], array());
362                 $units[$i]->unit = $question->unit[$i];
363                 $DB->insert_record('question_numerical_units', $units[$i]);
364             }
365         }
366         unset($question->multiplier, $question->unit);
368         $result->units = &$units;
369         return $result;
370     }
372     function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
373         $state->responses = array();
374         $state->responses['answer'] =  '';
375         $state->responses['unit'] = '';
377         return true;
378     }
379     function restore_session_and_responses(&$question, &$state) {
380        if(false === strpos($state->responses[''], '|||||')){
381              $state->responses['answer']= $state->responses[''];
382              $state->responses['unit'] = '';
383              $this->split_old_answer($state->responses[''], $question->options->units, $state->responses['answer'] ,$state->responses['unit'] );
384        }else {
385             $responses = explode('|||||', $state->responses['']);
386             $state->responses['answer']= $responses[0];
387             $state->responses['unit'] = $responses[1];
388        }
390        return true;
391     }
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                 }
400             }
401         return 0 ;
402     }
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)) {
412             if(isset($responseparts[5]) ){
413                 $unit = $responseparts[5] ;
414             }
415             if(isset($responseparts[1]) ){
416                 $answer = $responseparts[1] ;
417             }
418         }
419         return ;
420     }
423     function save_session_and_responses(&$question, &$state) {
424         global $DB;
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
435         $DB->set_field('question_states', 'answer', $responses, array('id' => $state->id));
436         return true;
437     }
439     /**
440      * Deletes question from the question-type specific tables
441      *
442      * @return boolean Success/Failure
443      * @param object $question  The question being deleted
444      */
445     function delete_question($questionid) {
446         global $DB;
447         $DB->delete_records("question_numerical", array("question" => $questionid));
448         $DB->delete_records("question_numerical_options", array("question" => $questionid));
449         $DB->delete_records("question_numerical_units", array("question" => $questionid));
450         return true;
451     }
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) {
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>";
461         $context = $this->get_context_by_category_id($question->category);
462         $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
463         $formatoptions = new stdClass;
464         $formatoptions->noclean = true;
465         $formatoptions->para = false;
466         $nameprefix = $question->name_prefix;
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);
471         /// Print question text and media
473         $questiontext = format_text($question->questiontext,
474                 $question->questiontextformat,
475                 $formatoptions, $cmoptions->course);
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
480         // If $question->options->showunits == NUMERICALQUESTIONUNITTEXTDISPLAY
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.
486         $nameanswer = "name=\"".$question->name_prefix."answer\"";
487         $nameunit   = "name=\"".$question->name_prefix."unit\"";
488         // put old answer data in $state->responses['answer'] and $state->responses['unit']
489         if (isset($state->responses['']) && $state->responses[''] != '' && !isset($state->responses['answer'])){
490               $this->split_old_answer($state->responses[''], $question->options->units, $state->responses['answer'] ,$state->responses['unit'] );
491         }
492         // prepare the values of the input elements to be dispalyed answer i.e. number  and unit 
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']).'" ';
500         } else {
501             $valueunit = ' value="" ';
502             if ($question->options->showunits == NUMERICALQUESTIONUNITTEXTDISPLAY ){
503               $valueunit = ' value="'.s($question->options->units[0]->unit).'" ';
504             }
505         }
507         $feedback = '';
508         $class = '';
509         $classunit = '' ;
510         $classunitvalue = '' ;
511         $feedbackimg = '';
512         $feedbackimgunit = '' ;
513         $answerasterisk = false ;
514         $response = '' ;
515         $valid_numerical_unit = false ;
516         $valid_numerical_unit_index = -1 ;
517         $unit_in_numerical_answer = false ;
518         $rawgrade = 0 ;
519         if ($options->feedback) {
520             $class = question_get_feedback_class(0);
521             $classunit = question_get_feedback_class(0);
522             $feedbackimg = question_get_feedback_image(0);
523             $feedbackimgunit = question_get_feedback_image(0);
524             $classunitvalue = 0 ;
525             $valid_numerical_unit_index = -1 ;
526             // if there is unit in answer and unitgradingtype = 0 
527             // the grade is 0            
528             //this is OK for the first answer with a good response
529             // having to test for * so response as long as not empty
530            // $response = $this->extract_numerical_response($state->responses['answer']);
531             // test for a greater than 0 grade    
532             foreach($question->options->answers as $answer) {
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
539                     $class = question_get_feedback_class($answer->fraction);
540                     $feedbackimg = question_get_feedback_image($answer->fraction);
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 ;
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 ;
588                             }else {
589                                 $unit_in_numerical_answer = false ;
590                             }
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 ;
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                         }    
634                     }
635                     //     echo "<p> dans  valid_numerical_unit_index $valid_numerical_unit_index  </p>";
636                     if ($answer->feedback) {
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);
639                     }
640                     
641                     break;
642                 }
643             }
644                                     
645                                        
646     }
647    // echo "<p> rawgrade $rawgrade classunit $classunit valid_numerical_unit_index $valid_numerical_unit_index ".$feedbackimgunit."</p>";
648         $state->options->raw_unitpenalty = 0 ;
649         $raw_unitpenalty = 0 ;
650         if( $question->options->showunits == NUMERICALQUESTIONUNITNODISPLAY ||
651                 $question->options->showunits == NUMERICALQUESTIONUNITTEXTDISPLAY ) {
652                     $classunitvalue = 1 ;
653         }
655         if(! $answerasterisk  && $question->options->unitgradingtype != 0 && (! $valid_numerical_unit || $unit_in_numerical_answer)){
656             if($question->options->unitgradingtype == 1){
657                 $raw_unitpenalty = $question->options->unitpenalty * $rawgrade ;
658             }else {
659                 $raw_unitpenalty = $question->options->unitpenalty * $question->maxgrade;
660             }
661             $state->options->raw_unitpenalty = $raw_unitpenalty ;
662         }
664         /// Removed correct answer, to be displayed later MDL-7496
665         include("$CFG->dirroot/question/type/numerical/display.html");
666     }
669     function compare_responses(&$question, $state, $teststate) {
671                if ($question->options->showunits == NUMERICALQUESTIONUNITMULTICHOICEDISPLAY && isset($question->options->units) && isset($state->responses['unit']) && isset($question->options->units[$state->responses['unit']] )){
672             $state->responses['unit']=$question->options->units[$state->responses['unit']]->unit;
673         };
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         }
691         if ( isset($responses)  && isset($testresponses )) {
693             return $responses == $testresponses ;
694         }
695         return false;
696     }
698     /**
699      * Checks whether a response matches a given answer, taking the tolerance
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.
703      * if unit != -1 then the test is done only on this unit
704      */
705     function test_response(&$question, &$state, $answer ) {
706         // Deal with the match anything answer.
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>";
709         if ($answer->answer === '*') {
710             return true;
711         }
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[''];
718             }
719             $answertotest = $state->responses['answer'];
720             // values coming from  NUMERICALQUESTIONUNITTEXTINPUTDISPLAY
721             // or NUMERICALQUESTIONUNITTEXTDISPLAY as unit hidden HTML element
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             }
731             if(isset($state->responses['unit'])) {
732                 $answertotest .= $state->responses['unit'] ;    
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;
794         }
795         return false;
796     }
798     /**
799     * Performs response processing and grading
800     * The function was redefined for handling correctly the two parts
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.
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     */
821     function grade_responses(&$question, &$state, $cmoptions) {
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[''])){
825            $state->responses['answer'] =  $state->responses[''];
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'] );
829         }
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
833         // however it is used to detect good or bad numbers but also
834         // gives false if there is a unit 
835         $state->raw_grade = 0;
836         $valid_numerical_unit = false ;
837         $break = 0 ;
838         $unittested = '';
839         $hasunits = 0 ;
840        // $response = $this->extract_numerical_response($state->responses['answer']);
841         $answerasterisk = false ;
843         $break = 0 ;
844         foreach($question->options->answers as $answer) {
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                         }
875                     }
876                 }
877                 break ;
878             } 
879         }
880         // apply unit penalty
881         $raw_unitpenalty = 0 ;
882         if($question->options->unitgradingtype != 0 && !empty($question->options->unitpenalty)&& $valid_numerical_unit != true ){
883             if($question->options->unitgradingtype == 1){
884                 $raw_unitpenalty = $question->options->unitpenalty * $state->raw_grade ;
885             }else {
886                 $raw_unitpenalty = $question->options->unitpenalty * $question->maxgrade;
887             }
888             $state->raw_grade -= $raw_unitpenalty ;
889         }
890         
891         // Make sure we don't assign negative or too high marks.
892         $state->raw_grade = min(max((float) $state->raw_grade,
893                             0.0), 1.0) * $question->maxgrade;
894         
895         // Update the penalty.
896         $state->penalty = $question->penalty * $question->maxgrade;
898         // mark the state as graded
899         $state->event = ($state->event ==  QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
901         return true;
902     }
905     function get_correct_responses(&$question, &$state) {
906         $correct = parent::get_correct_responses($question, $state);
907         $unit = $this->get_default_numerical_unit($question);
908         if (isset($correct['']) && $correct[''] != '*' && $unit) {
909             $correct[''] .= ' '.$unit->unit;
910         }
911         return $correct;
912     }
914     // ULPGC ecastro
915     function get_all_responses(&$question, &$state) {
916         $result = new stdClass;
917         $answers = array();
918         $unit = $this->get_default_numerical_unit($question);
919         if (is_array($question->options->answers)) {
920             foreach ($question->options->answers as $aid=>$answer) {
921                 $r = new stdClass;
922                 $r->answer = $answer->answer;
923                 $r->credit = $answer->fraction;
924                 $this->get_tolerance_interval($answer);
925                 if ($r->answer != '*' && $unit) {
926                     $r->answer .= ' ' . $unit->unit;
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             }
935         }
936         $result->id = $question->id;
937         $result->responses = $answers;
938         return $result;
939     }
940     function get_actual_response($question, $state) {
941        if (!empty($state->responses) && !empty($state->responses[''])) {
942            if(false === strpos($state->responses[''], '|||||')){
943                 $responses[] = $state->responses[''];
944             }else {
945                 $resp = explode('|||||', $state->responses['']);
946                 $responses[] = $resp[0].$resp[1];
947             }
948        } else {
949            $responses[] = '';
950         }
952        return $responses;
953     }
956     function get_tolerance_interval(&$answer) {
957         // No tolerance
958         if (empty($answer->tolerance)) {
959             $answer->tolerance = 0;
960         }
962         // Calculate the interval of correct responses (min/max)
963         if (!isset($answer->tolerancetype)) {
964             $answer->tolerancetype = 2; // nominal
965         }
967         // We need to add a tiny fraction depending on the set precision to make the
968         // comparison work correctly. Otherwise seemingly equal values can yield
969         // false. (fixes bug #3225)
970         $tolerance = (float)$answer->tolerance + ("1.0e-".ini_get('precision'));
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;
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;
981             case '2': case 'nominal':
982                 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
983                 // $answer->tolerance 0 or something else
984                 if ((float)$answer->tolerance == 0.0  &&  abs((float)$answer->answer) <= $tolerance ){
985                     $tolerance = (float) ("1.0e-".ini_get('precision')) * abs((float)$answer->answer) ; //tiny fraction
986                 } else if ((float)$answer->tolerance != 0.0 && abs((float)$answer->tolerance) < abs((float)$answer->answer) &&  abs((float)$answer->answer) <= $tolerance){
987                     $tolerance = (1+("1.0e-".ini_get('precision')) )* abs((float) $answer->tolerance) ;//tiny fraction
988                }
990                 $max = $answer->answer + $tolerance;
991                 $min = $answer->answer - $tolerance;
992                 break;
993             case '3': case 'geometric':
994                 $quotient = 1 + abs($tolerance);
995                 $max = $answer->answer * $quotient;
996                 $min = $answer->answer / $quotient;
997                 break;
998             default:
999                 print_error('unknowntolerance', 'question', '', $answer->tolerancetype);
1000         }
1002         $answer->min = $min;
1003         $answer->max = $max;
1004         return true;
1005     }
1007     /**
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      */
1016     function extract_numerical_response($rawresponse) {
1017         $extractedresponse = new stdClass() ;
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('', '');
1023         }else { // remove spaces and normalise , to a . .
1024             $replace = array('', '.');
1025         }
1026         $rawresponse = str_replace($search, $replace, $rawresponse);
1028          if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
1029                 $rawresponse, $responseparts)) {
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 = '';
1039         }
1040                 
1041         // Invalid number. Must be wrong.
1042         return clone($extractedresponse) ;
1043     }
1044     /**
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      */
1053     function apply_unit($rawresponse, $units) {
1054            //     echo"<p> rawresponse $rawresponse <pre>";print_r($units) ;echo"</pre></p>";
1056         // Make units more useful
1057         $tmpunits = array();
1058         foreach ($units as $unit) {
1059             $tmpunits[$unit->unit] = $unit->multiplier;
1060         }
1061         // remove spaces and normalise decimal places.
1062         $rawresponse = trim($rawresponse) ;
1063         $search  = array(' ', ',');
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('', '');
1067         }else { // remove spaces and normalise , to a . .
1068             $replace = array('', '.');
1069         }
1070         $rawresponse = str_replace($search, $replace, $rawresponse);
1073         // Apply any unit that is present.
1074         if (ereg('^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$',
1075                 $rawresponse, $responseparts)) {
1076            //     echo"<p> responseparts <pre>";print_r($responseparts) ;echo"</pre></p>";
1078             if (!empty($responseparts[5])) {
1080                 if (isset($tmpunits[$responseparts[5]])) {
1081                     // Valid number with unit.
1082                     return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
1083                 } else {
1084                     // Valid number with invalid unit. Must be wrong.
1085                     return false;
1086                 }
1088             } else {
1089                 // Valid number without unit.
1090                 return (float)$responseparts[1];
1091             }
1092         }
1093         // Invalid number. Must be wrong.
1094         return false;
1095     }
1097     /**
1098     * function used in function definition_inner()
1099     * of edit_..._form.php for
1100     * numerical, calculated, calculatedsimple
1101     */
1102     function add_units_options(&$mform, &$that){
1103         // Units are graded
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);
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'));
1119         $penaltygrp[] =& $mform->createElement('select', 'unitgradingtypes', '' , $unitgradingtypes );
1120         $mform->addGroup($penaltygrp, 'penaltygrp', get_string('unitpenalty', 'qtype_numerical'),' ' , false);
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         
1134         $mform->addElement('editor', 'instructions', get_string('instructions', 'qtype_numerical'), null, $that->editoroptions);
1135 //        $mform->addElement('static', 'separator1', '<HR/>', '<HR/>');
1136         // Units are not graded
1137         $showunits1grp = array();
1138         $mform->addElement('static', 'separator2', '<HR/>', '<HR/>');
1140         $mform->setType('unitpenalty', PARAM_NUMBER);
1141         $mform->setDefault('unitpenalty', 0.1);
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);
1146         $mform->setDefault('unitsleft', 0);
1147         $mform->setType('instructions', PARAM_RAW);
1148       //  $mform->addHelpButton('instructions', 'unituses', 'qtype_numerical');
1149         $mform->addHelpButton('instructions', 'numericalinstructions', 'qtype_numerical');
1150         $mform->disabledIf('penaltygrp', 'unitrole','eq','0');
1151         $mform->disabledIf('penaltygrp', 'unitrole','eq','1');
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');
1162     }
1164     /**
1165      * function used in in function definition_inner()
1166      * of edit_..._form.php for
1167      * numerical, calculated, calculatedsimple
1168      */
1169     function add_units_elements(& $mform,& $that) {
1170         $repeated = array();
1171         $repeated[] =& $mform->createElement('header', 'unithdr', get_string('unithdr', 'qtype_numerical', '{no}'));
1173         $repeated[] =& $mform->createElement('text', 'unit', get_string('unit', 'quiz'));
1174         $mform->setType('unit', PARAM_NOTAGS);
1176         $repeated[] =& $mform->createElement('text', 'multiplier', get_string('multiplier', 'quiz'));
1177         $mform->setType('multiplier', PARAM_NUMBER);
1179         if (isset($that->question->options)){
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}'));
1191         if ($mform->elementExists('multiplier[0]')){
1192             $firstunit =& $mform->getElement('multiplier[0]');
1193             $firstunit->freeze();
1194             $firstunit->setValue('1.0');
1195             $firstunit->setPersistantFreeze(true);
1196             $mform->addHelpButton('multiplier[0]', 'numericalmultiplier', 'qtype_numerical');
1197         }
1198     }
1200     /**
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){
1206         list($categoryid) = explode(',', $question->category);
1207         $context = $this->get_context_by_category_id($categoryid);
1209         if (isset($question->options)){
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             } 
1217             $default_values['unitpenalty'] = $question->options->unitpenalty ;
1218             switch ($question->options->showunits){
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 ;
1233                     break;
1234                 case 2 : // NUMERICALQUESTIONUNITTEXTDISPLAY
1235                     $default_values['unitrole'] = 1 ;
1236                 case 3 : // NUMERICALQUESTIONUNITNODISPLAY
1237                     $default_values['unitrole'] = 0 ;
1238                   //  $default_values['showunits1'] = $question->options->showunits ;
1239                     break;
1240             }
1241             $default_values['unitsleft'] = $question->options->unitsleft ;
1242          //   $question->unitrole = $default_values['unitrole'] ;
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;
1260             if (isset($question->options->units)) {
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     }
1272     /**
1273       * function use in in function validation()
1274       * of edit_..._form.php for
1275       * numerical, calculated, calculatedsimple
1276       */
1278     function validate_numerical_options(& $data, & $errors){
1279         $units  = $data['unit'];
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             }
1291         if (($showunits == NUMERICALQUESTIONUNITTEXTINPUTDISPLAY) ||
1292                 ($showunits == NUMERICALQUESTIONUNITMULTICHOICEDISPLAY ) ||
1293                 ($showunits == NUMERICALQUESTIONUNITTEXTDISPLAY )){
1294            if (trim($units[0]) == ''){
1295              $errors['unit[0]'] = 'You must set a valid unit name' ;
1296             }
1297         }
1298         if ($showunits == NUMERICALQUESTIONUNITNODISPLAY ){
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         }
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                         }
1340                     }
1341                 }
1342             }
1344     }
1345  
1347     function valid_unit($rawresponse, $units) {
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));
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)) {
1362             if (!empty($responseparts[5])) {
1364                 if (isset($tmpunits[$responseparts[5]])) {
1365                     // Valid number with unit.
1366                     return true ; //(float)$responseparts[1] / $tmpunits[$responseparts[5]];
1367                 } else {
1368                     // Valid number with invalid unit. Must be wrong.
1369                     return false;
1370                 }
1372             } else {
1373                 // Valid number without unit.
1374                 return false ; //(float)$responseparts[1];
1375             }
1376         }
1377         // Invalid number. Must be wrong.
1378         return false;
1379     }
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;
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....');
1403         if ($courseid) {
1404             $course = $DB->get_record('course', array('id' => $courseid));
1405         }
1407         return $this->save_question($question, $form, $course);
1408     }
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);
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()) {
1429                     $newfile = new stdClass();
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()) {
1441                 $newfile = new stdClass();
1442                 $newfile->contextid = (int)$newcategory->contextid;
1443                 $fs->create_file_from_storedfile($newfile, $storedfile);
1444                 $storedfile->delete();
1445             }
1446         }
1447     }
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     }
1476 // INITIATION - Without this line the question type is not in use.
1477 question_register_questiontype(new question_numerical_qtype());
1478 if ( ! defined ("NUMERICALQUESTIONUNITTEXTINPUTDISPLAY")) {
1479     define("NUMERICALQUESTIONUNITTEXTINPUTDISPLAY",   0);
1481 if ( ! defined ("NUMERICALQUESTIONUNITMULTICHOICEDISPLAY")) {
1482     define("NUMERICALQUESTIONUNITMULTICHOICEDISPLAY",   1);
1484 if ( ! defined ("NUMERICALQUESTIONUNITTEXTDISPLAY")) {
1485     define("NUMERICALQUESTIONUNITTEXTDISPLAY",   2);
1487 if ( ! defined ("NUMERICALQUESTIONUNITNODISPLAY")) {
1488     define("NUMERICALQUESTIONUNITNODISPLAY",    3);