0d878988a062ecfda84189089d49de1a3746abb0
[moodle.git] / question / type / calculatedmulti / questiontype.php
1 <?php
3 /////////////////
4 // CALCULATED ///
5 /////////////////
7 /// QUESTION TYPE CLASS //////////////////
11 class question_calculatedmulti_qtype extends question_calculated_qtype {
13     // Used by the function custom_generator_tools:
14     var $calcgenerateidhasbeenadded = false;
15     public $virtualqtype = false;
17     function name() {
18         return 'calculatedmulti';
19     }
21     function has_wildcards_in_responses($question, $subqid) {
22         return true;
23     }
25     function requires_qtypes() {
26         return array('multichoice');
27     }
30     function save_question_options($question) {
31         //$options = $question->subtypeoptions;
32         // Get old answers:
33         global $CFG, $DB, $QTYPES ;
34         if (isset($question->answer) && !isset($question->answers)) {
35             $question->answers = $question->answer;
36         }
37         // calculated options
38         $update = true ; 
39         $options = $DB->get_record("question_calculated_options", array("question" => $question->id));
40         if (!$options) {
41             $update = false;
42             $options = new stdClass;
43             $options->question = $question->id;
44         }
45         $options->synchronize = $question->synchronize;
46         $options->single = $question->single;
47         $options->answernumbering = $question->answernumbering;
48         $options->shuffleanswers = $question->shuffleanswers;
49         $options->correctfeedback = trim($question->correctfeedback);
50         $options->partiallycorrectfeedback = trim($question->partiallycorrectfeedback);
51         $options->incorrectfeedback = trim($question->incorrectfeedback);
52         if ($update) {
53             if (!$DB->update_record("question_calculated_options", $options)) {
54                 $result->error = "Could not update calculated question options! (id=$options->id)";
55                 return $result;
56             }
57         } else {
58             if (!$DB->insert_record("question_calculated_options", $options)) {
59                 $result->error = "Could not insert calculated question options!";
60                 return $result;
61             }
62         }
64         // Get old versions of the objects
65         if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) {
66             $oldanswers = array();
67         }
69         if (!$oldoptions = $DB->get_records('question_calculated', array('question' => $question->id), 'answer ASC')) {
70             $oldoptions = array();
71         }
73         // Save the units.
74         $virtualqtype = $this->get_virtual_qtype( $question);
75        // $result = $virtualqtype->save_numerical_units($question);
76         if (isset($result->error)) {
77             return $result;
78         } else {
79             $units = &$result->units;
80         }
81         // Insert all the new answers
82         if (isset($question->answer) && !isset($question->answers)) {
83             $question->answers=$question->answer;
84         }
85         foreach ($question->answers as $key => $dataanswer) {
86             if (  trim($dataanswer) != '' ) {
87                 $answer = new stdClass;
88                 $answer->question = $question->id;
89                 $answer->answer = trim($dataanswer);
90                 $answer->fraction = $question->fraction[$key];
91                 $answer->feedback = trim($question->feedback[$key]);
93                 if ($oldanswer = array_shift($oldanswers)) {  // Existing answer, so reuse it
94                     $answer->id = $oldanswer->id;
95                     $DB->update_record("question_answers", $answer);
96                 } else { // This is a completely new answer
97                     $answer->id = $DB->insert_record("question_answers", $answer);
98                 }
100                 // Set up the options object
101                 if (!$options = array_shift($oldoptions)) {
102                     $options = new stdClass;
103                 }
104                 $options->question  = $question->id;
105                 $options->answer    = $answer->id;
106                 $options->tolerance = trim($question->tolerance[$key]);
107                 $options->tolerancetype  = trim($question->tolerancetype[$key]);
108                 $options->correctanswerlength  = trim($question->correctanswerlength[$key]);
109                 $options->correctanswerformat  = trim($question->correctanswerformat[$key]);
111                 // Save options
112                 if (isset($options->id)) { // reusing existing record
113                     $DB->update_record('question_calculated', $options);
114                 } else { // new options
115                     $DB->insert_record('question_calculated', $options);
116                 }
117             }
118         }
119         // delete old answer records
120         if (!empty($oldanswers)) {
121             foreach($oldanswers as $oa) {
122                 $DB->delete_records('question_answers', array('id' => $oa->id));
123             }
124         }
126         // delete old answer records
127         if (!empty($oldoptions)) {
128             foreach($oldoptions as $oo) {
129                 $DB->delete_records('question_calculated', array('id' => $oo->id));
130             }
131         }
132       //  $result = $QTYPES['numerical']->save_numerical_options($question);
133       //  if (isset($result->error)) {
134       //      return $result;
135       //  }
138         if( isset($question->import_process)&&$question->import_process){
139             $this->import_datasets($question);
140          }
141         // Report any problems.
142         if (!empty($result->notice)) {
143             return $result;
144         }
145         return true;
146     }
148     function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
149         // Find out how many datasets are available
150         global $CFG, $DB, $QTYPES, $OUTPUT ;
151         if(!$maxnumber = (int)$DB->get_field_sql(
152                             "SELECT MIN(a.itemcount)
153                             FROM {question_dataset_definitions} a,
154                                  {question_datasets} b
155                             WHERE b.question = ?
156                             AND   a.id = b.datasetdefinition", array($question->id))) {
157             print_error('cannotgetdsforquestion', 'question', '', $question->id);
158         }
159                     $sql = "SELECT i.*
160                     FROM {question_datasets} d,
161                          {question_dataset_definitions} i
162                     WHERE d.question = ?
163                     AND   d.datasetdefinition = i.id  
164                     AND   i.category != 0 
165                    ";
166         if (!$question->options->synchronize || !$records = $DB->get_records_sql($sql, array($question->id))) {
167             $synchronize_calculated  =  false ; 
168         }else {
169            // i.e records is true so test coherence
170            $coherence = true ;
171                 $a = new stdClass ;
172                 $a->qid = $question->id ;
173                 $a->qcat = $question->category ;
174            foreach($records as $def ){
175                 if ($def->category != $question->category){
176                     $a->name = $def->name;
177                     $a->sharedcat = $def->category ;
178                     $coherence = false ;
179                     break;
180                 }
181             }
182             if(!$coherence){
183                          echo $OUTPUT->notification(get_string('nocoherencequestionsdatyasetcategory','qtype_calculated',$a));
184           } 
185             
186             $synchronize_calculated  = true ; 
187         }    
189         // Choose a random dataset
190         // maxnumber sould not be breater than 100
191         if ($maxnumber > CALCULATEDQUESTIONMAXITEMNUMBER ){
192             $maxnumber = CALCULATEDQUESTIONMAXITEMNUMBER ;
193         }
194         if ( $synchronize_calculated === false ) {
195             $state->options->datasetitem = rand(1, $maxnumber);
196         }else{
197             $state->options->datasetitem = intval( $maxnumber * substr($attempt->timestart,-2) /100 ) ;            
198             if ($state->options->datasetitem < 1) {
199                 $state->options->datasetitem =1 ;
200             } else if ($state->options->datasetitem > $maxnumber){
201                 $state->options->datasetitem = $maxnumber ;
202             }
203            
204         };  
205         $state->options->dataset =
206          $this->pick_question_dataset($question,$state->options->datasetitem);
207                     // create an array of answerids ??? why so complicated ???
208             $answerids = array_values(array_map(create_function('$val',
209              'return $val->id;'), $question->options->answers));
210             // Shuffle the answers if required
211             if (!empty($cmoptions->shuffleanswers) and !empty($question->options->shuffleanswers)) {
212                $answerids = swapshuffle($answerids);
213             }
214             $state->options->order = $answerids;
215             // Create empty responses
216             if ($question->options->single) {
217                 $state->responses = array('' => '');
218             } else {
219                 $state->responses = array();
220             }
221             return true;
222         
223     }
224     
225     function save_session_and_responses(&$question, &$state) {
226         global $DB;
227         $responses = 'dataset'.$state->options->datasetitem.'-' ;       
228         $responses .= implode(',', $state->options->order) . ':';
229         $responses .= implode(',', $state->responses);
230          
231         // Set the legacy answer field        
232         if (!$DB->set_field('question_states', 'answer', $responses, array('id'=> $state->id))) {
233             return false;
234         }
235         return true;
236     }
238     function create_runtime_question($question, $form) {
239         $question = default_questiontype::create_runtime_question($question, $form);
240         $question->options->answers = array();
241         foreach ($form->answers as $key => $answer) {
242             $a->answer              = trim($form->answer[$key]);
243             $a->fraction              = $form->fraction[$key];//new
244            $a->tolerance           = $form->tolerance[$key];
245             $a->tolerancetype       = $form->tolerancetype[$key];
246             $a->correctanswerlength = $form->correctanswerlength[$key];
247             $a->correctanswerformat = $form->correctanswerformat[$key];
248             $question->options->answers[] = clone($a);
249         }
251         return $question;
252     }
258     function convert_answers (&$question, &$state){
259             foreach ($question->options->answers as $key => $answer) {
260                 $answer->answer = $this->substitute_variables($answer->answer, $state->options->dataset);
261                 //evaluate the equations i.e {=5+4)
262                 $qtext = "";
263                 $qtextremaining = $answer->answer ;
264              //   while  (preg_match('~\{(=)|%[[:digit]]\.=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
265                 while  (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
267                     $qtextsplits = explode($regs1[0], $qtextremaining, 2);
268                     $qtext =$qtext.$qtextsplits[0];
269                     $qtextremaining = $qtextsplits[1];
270                     if (empty($regs1[1])) {
271                             $str = '';
272                         } else {
273                             if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
274                                 $str=$formulaerrors ;
275                             }else {
276                                 eval('$str = '.$regs1[1].';');
277                        $texteval= qtype_calculated_calculate_answer(
278                      $str, $state->options->dataset, $answer->tolerance,
279                      $answer->tolerancetype, $answer->correctanswerlength,
280                         $answer->correctanswerformat, '');
281                         $str = $texteval->answer;
282                             }
283                         }
284                         $qtext = $qtext.$str ;
285                 }
286                 $answer->answer = $qtext.$qtextremaining ; ;
287             }
288         }
290     function get_default_numerical_unit($question,$virtualqtype){
291                 $unit = '';
292             return $unit ;        
293     }    
294     function grade_responses(&$question, &$state, $cmoptions) {
295         // Forward the grading to the virtual qtype
296         // We modify the question to look like a multichoice question
297         // for grading nothing to do 
298 /*        $numericalquestion = fullclone($question);
299        foreach ($numericalquestion->options->answers as $key => $answer) {
300             $answer = $numericalquestion->options->answers[$key]->answer; // for PHP 4.x
301           $numericalquestion->options->answers[$key]->answer = $this->substitute_variables_and_eval($answer,
302              $state->options->dataset);
303        }*/
304          $virtualqtype = $this->get_virtual_qtype( $question);
305         return $virtualqtype->grade_responses($question, $state, $cmoptions) ;
306     }
310     // ULPGC ecastro
311     function get_actual_response(&$question, &$state) {
312         // Substitute variables in questiontext before giving the data to the
313         // virtual type
314         $virtualqtype = $this->get_virtual_qtype( $question);
315         $unit = '' ;//$virtualqtype->get_default_numerical_unit($question);
317         // We modify the question to look like a multichoice question
318         $numericalquestion = clone($question);
319         $this->convert_answers ($numericalquestion, $state);
320         $this->convert_questiontext ($numericalquestion, $state);
321      /*   $numericalquestion->questiontext = $this->substitute_variables_and_eval(
322                                   $numericalquestion->questiontext, $state->options->dataset);*/
323         $responses = $virtualqtype->get_all_responses($numericalquestion, $state);
324         $response = reset($responses->responses);
325         $correct = $response->answer.' : ';
327         $responses = $virtualqtype->get_actual_response($numericalquestion, $state);
329         foreach ($responses as $key=>$response){
330             $responses[$key] = $correct.$response;
331         }
333         return $responses;
334     }
336     function create_virtual_qtype() {
337         global $CFG;
338             require_once("$CFG->dirroot/question/type/multichoice/questiontype.php");
339             return new question_multichoice_qtype();
340     }
343     function comment_header($question) {
344         //$this->get_question_options($question);
345         $strheader = '';
346         $delimiter = '';
348         $answers = $question->options->answers;
350         foreach ($answers as $key => $answer) {
351             if (is_string($answer)) {
352                 $strheader .= $delimiter.$answer;
353             } else {
354                 $strheader .= $delimiter.$answer->answer;
355             }
356                 $delimiter = '<br/>';            
357         }
358         return $strheader;
359     }
361     function comment_on_datasetitems($qtypeobj,$questionid,$questiontext, $answers,$data, $number) { //multichoice_
362         global $DB;
363         $comment = new stdClass;
364         $comment->stranswers = array();
365         $comment->outsidelimit = false ;
366         $comment->answers = array();
367         /// Find a default unit:
368     /*    if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units', array('question'=> $questionid, 'multiplier' => 1.0))) {
369             $unit = $unit->unit;
370         } else {
371             $unit = '';
372         }*/
374         $answers = fullclone($answers);
375         $strmin = get_string('min', 'quiz');
376         $strmax = get_string('max', 'quiz');
377         $errors = '';
378         $delimiter = ': ';
379         foreach ($answers as $key => $answer) {
380                 $answer->answer = $this->substitute_variables($answer->answer, $data);
381                 //evaluate the equations i.e {=5+4)
382                 $qtext = "";
383                 $qtextremaining = $answer->answer ;
384                 while  (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
385                     $qtextsplits = explode($regs1[0], $qtextremaining, 2);
386                     $qtext =$qtext.$qtextsplits[0];
387                     $qtextremaining = $qtextsplits[1];
388                     if (empty($regs1[1])) {
389                             $str = '';
390                         } else {
391                             if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
392                                 $str=$formulaerrors ;
393                             }else {
394                                 eval('$str = '.$regs1[1].';');
395                             }
396                         }
397                         $qtext = $qtext.$str ;
398                 }
399                 $answer->answer = $qtext.$qtextremaining ; ;
400                 $comment->stranswers[$key]= $answer->answer ;
401             
402             
403           /*  $formula = $this->substitute_variables($answer->answer,$data);
404             $formattedanswer = qtype_calculated_calculate_answer(
405                     $answer->answer, $data, $answer->tolerance,
406                     $answer->tolerancetype, $answer->correctanswerlength,
407                     $answer->correctanswerformat, $unit);
408                     if ( $formula === '*'){
409                         $answer->min = ' ';
410                         $formattedanswer->answer = $answer->answer ;
411                     }else {
412                         eval('$answer->answer = '.$formula.';') ;
413                         $virtualqtype->get_tolerance_interval($answer);
414                     } 
415             if ($answer->min === '') {
416                 // This should mean that something is wrong
417                 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
418             } else if ($formula === '*'){
419                 $comment->stranswers[$key] = $formula.' = '.get_string('anyvalue','qtype_calculated').'<br/><br/><br/>';
420             }else{
421                 $comment->stranswers[$key]= $formula.' = '.$formattedanswer->answer.'<br/>' ;
422                 $comment->stranswers[$key] .= $strmin. $delimiter.$answer->min.'---';
423                 $comment->stranswers[$key] .= $strmax.$delimiter.$answer->max;
424                 $comment->stranswers[$key] .='<br/>';
425                 $correcttrue->correct = $formattedanswer->answer ;
426                 $correcttrue->true = $answer->answer ;
427                 if ($formattedanswer->answer < $answer->min || $formattedanswer->answer > $answer->max){
428                     $comment->outsidelimit = true ;
429                     $comment->answers[$key] = $key;
430                     $comment->stranswers[$key] .=get_string('trueansweroutsidelimits','qtype_calculated',$correcttrue);//<span class="error">ERROR True answer '..' outside limits</span>';
431                 }else {
432                     $comment->stranswers[$key] .=get_string('trueanswerinsidelimits','qtype_calculated',$correcttrue);//' True answer :'.$calculated->trueanswer.' inside limits';
433                 }
434                 $comment->stranswers[$key] .='';
435             }*/
436         }
437         return fullclone($comment);
438     }
444     function get_correct_responses1(&$question, &$state) {
445         $virtualqtype = $this->get_virtual_qtype( $question);
446     /*    if ($question->options->multichoice != 1 ) {
447             if($unit = $virtualqtype->get_default_numerical_unit($question)){
448                  $unit = $unit->unit;
449             } else {
450                 $unit = '';
451             }
452             foreach ($question->options->answers as $answer) {
453                 if (((int) $answer->fraction) === 1) {
454                     $answernumerical = qtype_calculated_calculate_answer(
455                      $answer->answer, $state->options->dataset, $answer->tolerance,
456                      $answer->tolerancetype, $answer->correctanswerlength,
457                         $answer->correctanswerformat, ''); // remove unit
458                         $correct = array('' => $answernumerical->answer);
459                         $correct['answer']= $correct[''];
460                     if (isset($correct['']) && $correct[''] != '*' && $unit ) {
461                             $correct[''] .= ' '.$unit;
462                             $correct['unit']= $unit;
463                     }
464                     return $correct;
465                 }
466             }
467         }else{**/
468             return $virtualqtype->get_correct_responses($question, $state) ;
469        // }
470         return null;
471     }
473     function get_virtual_qtype() {
474         global $QTYPES;
475     //    if ( isset($question->options->multichoice) && $question->options->multichoice == '1'){
476             $this->virtualqtype =& $QTYPES['multichoice'];
477      //   }else {
478      //       $this->virtualqtype =& $QTYPES['numerical'];
479      //   }
480         return $this->virtualqtype;
481     }
484         /**
485    * Runs all the code required to set up and save an essay question for testing purposes.
486    * Alternate DB table prefix may be used to facilitate data deletion.
487    */
488   function generate_test($name, $courseid = null) {
489       global $DB;
490       list($form, $question) = parent::generate_test($name, $courseid);
491       $form->feedback = 1;
492       $form->multiplier = array(1, 1);
493       $form->shuffleanswers = 1;
494       $form->noanswers = 1;
495       $form->qtype ='calculatedmulti';
496       $question->qtype ='calculatedmulti';
497       $form->answers = array('{a} + {b}');
498       $form->fraction = array(1);
499       $form->tolerance = array(0.01);
500       $form->tolerancetype = array(1);
501       $form->correctanswerlength = array(2);
502       $form->correctanswerformat = array(1);
503       $form->questiontext = "What is {a} + {b}?";
505       if ($courseid) {
506           $course = $DB->get_record('course', array('id'=> $courseid));
507       }
509       $new_question = $this->save_question($question, $form, $course);
511       $dataset_form = new stdClass();
512       $dataset_form->nextpageparam["forceregeneration"]= 1;
513       $dataset_form->calcmin = array(1 => 1.0, 2 => 1.0);
514       $dataset_form->calcmax = array(1 => 10.0, 2 => 10.0);
515       $dataset_form->calclength = array(1 => 1, 2 => 1);
516       $dataset_form->number = array(1 => 5.4 , 2 => 4.9);
517       $dataset_form->itemid = array(1 => '' , 2 => '');
518       $dataset_form->calcdistribution = array(1 => 'uniform', 2 => 'uniform');
519       $dataset_form->definition = array(1 => "1-0-a",
520                                         2 => "1-0-b");
521       $dataset_form->nextpageparam = array('forceregeneration' => false);
522       $dataset_form->addbutton = 1;
523       $dataset_form->selectadd = 1;
524       $dataset_form->courseid = $courseid;
525       $dataset_form->cmid = 0;
526       $dataset_form->id = $new_question->id;
527       $this->save_dataset_items($new_question, $dataset_form);
529       return $new_question;
530   }
532 //// END OF CLASS ////
534 //////////////////////////////////////////////////////////////////////////
535 //// INITIATION - Without this line the question type is not in use... ///
536 //////////////////////////////////////////////////////////////////////////
537 question_register_questiontype(new question_calculatedmulti_qtype());
539 if ( ! defined ("CALCULATEDMULTI")) {
540     define("CALCULATEDMULTI",    "calculatedmulti");