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