MDL-16094 File storage conversion Quiz and Questions
[moodle.git] / question / type / calculatedmulti / 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 // CALCULATED ///
20 /////////////////
22 /// QUESTION TYPE CLASS //////////////////
24 class question_calculatedmulti_qtype extends question_calculated_qtype {
26     // Used by the function custom_generator_tools:
27     public $calcgenerateidhasbeenadded = false;
28     public $virtualqtype = false;
30     function name() {
31         return 'calculatedmulti';
32     }
34     function has_wildcards_in_responses($question, $subqid) {
35         return true;
36     }
38     function requires_qtypes() {
39         return array('multichoice');
40     }
43     function save_question_options($question) {
44         global $CFG, $DB, $QTYPES ;
45         $context = $question->context;
46         if (isset($question->answer) && !isset($question->answers)) {
47             $question->answers = $question->answer;
48         }
49         // calculated options
50         $update = true ;
51         $options = $DB->get_record("question_calculated_options", array("question" => $question->id));
52         if (!$options) {
53             $update = false;
54             $options = new stdClass;
55             $options->question = $question->id;
56         }
57         $options->synchronize = $question->synchronize;
58         $options->single = $question->single;
59         $options->answernumbering = $question->answernumbering;
60         $options->shuffleanswers = $question->shuffleanswers;
62         // save question feedback files
63         foreach (array('correct', 'partiallycorrect', 'incorrect') as $feedbacktype) {
64             $feedbackname = $feedbacktype . 'feedback';
65             $feedbackformat = $feedbackname . 'format';
66             $feedback = $question->$feedbackname;
67             $options->$feedbackformat = $feedback['format'];
68             $options->$feedbackname = file_save_draft_area_files($feedback['itemid'], $context->id, 'qtype_calculatedmulti', $feedbackname, $question->id, self::$fileoptions, trim($feedback['text']));
69         }
71         if ($update) {
72             $DB->update_record("question_calculated_options", $options);
73         } else {
74             $DB->insert_record("question_calculated_options", $options);
75         }
77         // Get old versions of the objects
78         if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) {
79             $oldanswers = array();
80         }
82         if (!$oldoptions = $DB->get_records('question_calculated', array('question' => $question->id), 'answer ASC')) {
83             $oldoptions = array();
84         }
86         // Save the units.
87         $virtualqtype = $this->get_virtual_qtype($question);
88         // TODO: What is this?
89         // $result = $virtualqtype->save_numerical_units($question);
90         if (isset($result->error)) {
91             return $result;
92         } else {
93             $units = &$result->units;
94         }
95         // Insert all the new answers
96         if (isset($question->answer) && !isset($question->answers)) {
97             $question->answers = $question->answer;
98         }
99         foreach ($question->answers as $key => $dataanswer) {
100             if ( trim($dataanswer) != '' ) {
101                 $answer = new stdClass;
102                 $answer->question = $question->id;
103                 $answer->answer = trim($dataanswer);
104                 $answer->fraction = $question->fraction[$key];
105                 $answer->feedback = trim($question->feedback[$key]['text']);
106                 $answer->feedbackformat = $question->feedback[$key]['format'];
108                 if ($oldanswer = array_shift($oldanswers)) {  // Existing answer, so reuse it
109                     $answer->id = $oldanswer->id;
110                     $answer->feedback = file_save_draft_area_files($question->feedback[$key]['itemid'], $context->id, 'question', 'answerfeedback', $answer->id, self::$fileoptions, $answer->feedback);
111                     $DB->update_record("question_answers", $answer);
112                 } else { // This is a completely new answer
113                     $answer->id = $DB->insert_record("question_answers", $answer);
114                     $feedbacktext = file_save_draft_area_files($question->feedback[$key]['itemid'], $context->id, 'question', 'answerfeedback', $answer->id, self::$fileoptions, $answer->feedback);
115                     $DB->set_field('question_answers', 'feedback', $feedbacktext, array('id'=>$answer->id));
116                 }
118                 // Set up the options object
119                 if (!$options = array_shift($oldoptions)) {
120                     $options = new stdClass;
121                 }
122                 $options->question  = $question->id;
123                 $options->answer    = $answer->id;
124                 $options->tolerance = trim($question->tolerance[$key]);
125                 $options->tolerancetype  = trim($question->tolerancetype[$key]);
126                 $options->correctanswerlength  = trim($question->correctanswerlength[$key]);
127                 $options->correctanswerformat  = trim($question->correctanswerformat[$key]);
129                 // Save options
130                 if (isset($options->id)) { // reusing existing record
131                     $DB->update_record('question_calculated', $options);
132                 } else { // new options
133                     $DB->insert_record('question_calculated', $options);
134                 }
135             }
136         }
137         // delete old answer records
138         if (!empty($oldanswers)) {
139             foreach($oldanswers as $oa) {
140                 $DB->delete_records('question_answers', array('id' => $oa->id));
141             }
142         }
144         // delete old answer records
145         if (!empty($oldoptions)) {
146             foreach($oldoptions as $oo) {
147                 $DB->delete_records('question_calculated', array('id' => $oo->id));
148             }
149         }
150         //  $result = $QTYPES['numerical']->save_numerical_options($question);
151         //  if (isset($result->error)) {
152         //      return $result;
153         //  }
156         if( isset($question->import_process)&&$question->import_process){
157             $this->import_datasets($question);
158         }
159         // Report any problems.
160         if (!empty($result->notice)) {
161             return $result;
162         }
163         return true;
164     }
166     function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
167         // Find out how many datasets are available
168         global $CFG, $DB, $QTYPES, $OUTPUT ;
169         $maxnumber = (int)$DB->get_field_sql(
170             "SELECT MIN(a.itemcount)
171                FROM {question_dataset_definitions} a, {question_datasets} b
172               WHERE b.question = ? AND a.id = b.datasetdefinition", array($question->id));
173         if (!$maxnumber) {
174             print_error('cannotgetdsforquestion', 'question', '', $question->id);
175         }
176         $sql = "SELECT i.*
177                   FROM {question_datasets} d, {question_dataset_definitions} i
178                  WHERE d.question = ? AND d.datasetdefinition = i.id AND i.category != 0";
179         if (!$question->options->synchronize || !$records = $DB->get_records_sql($sql, array($question->id))) {
180             $synchronize_calculated  =  false ;
181         } else {
182             // i.e records is true so test coherence
183             $coherence = true ;
184             $a = new stdClass ;
185             $a->qid = $question->id ;
186             $a->qcat = $question->category ;
187             foreach($records as $def ){
188                 if ($def->category != $question->category){
189                     $a->name = $def->name;
190                     $a->sharedcat = $def->category ;
191                     $coherence = false ;
192                     break;
193                 }
194             }
195             if(!$coherence){
196                 echo $OUTPUT->notification(get_string('nocoherencequestionsdatyasetcategory','qtype_calculated',$a));
197             }
199             $synchronize_calculated  = true ;
200         }
202         // Choose a random dataset
203         // maxnumber sould not be breater than 100
204         if ($maxnumber > CALCULATEDQUESTIONMAXITEMNUMBER ){
205             $maxnumber = CALCULATEDQUESTIONMAXITEMNUMBER ;
206         }
207         if ( $synchronize_calculated === false ) {
208             $state->options->datasetitem = rand(1, $maxnumber);
209         }else{
210             $state->options->datasetitem = intval( $maxnumber * substr($attempt->timestart,-2) /100 ) ;
211             if ($state->options->datasetitem < 1) {
212                 $state->options->datasetitem =1 ;
213             } else if ($state->options->datasetitem > $maxnumber){
214                 $state->options->datasetitem = $maxnumber ;
215             }
217         };
218         $state->options->dataset =
219             $this->pick_question_dataset($question,$state->options->datasetitem);
220         // create an array of answerids ??? why so complicated ???
221         $answerids = array_values(array_map(create_function('$val',
222             'return $val->id;'), $question->options->answers));
223         // Shuffle the answers if required
224         if (!empty($cmoptions->shuffleanswers) and !empty($question->options->shuffleanswers)) {
225             $answerids = swapshuffle($answerids);
226         }
227         $state->options->order = $answerids;
228         // Create empty responses
229         if ($question->options->single) {
230             $state->responses = array('' => '');
231         } else {
232             $state->responses = array();
233         }
234         return true;
235     }
237     function save_session_and_responses(&$question, &$state) {
238         global $DB;
239         $responses = 'dataset'.$state->options->datasetitem.'-' ;
240         $responses .= implode(',', $state->options->order) . ':';
241         $responses .= implode(',', $state->responses);
243         // Set the legacy answer field
244         if (!$DB->set_field('question_states', 'answer', $responses, array('id'=> $state->id))) {
245             return false;
246         }
247         return true;
248     }
250     function create_runtime_question($question, $form) {
251         $question = default_questiontype::create_runtime_question($question, $form);
252         $question->options->answers = array();
253         foreach ($form->answers as $key => $answer) {
254             $a->answer              = trim($form->answer[$key]);
255             $a->fraction              = $form->fraction[$key];//new
256             $a->tolerance           = $form->tolerance[$key];
257             $a->tolerancetype       = $form->tolerancetype[$key];
258             $a->correctanswerlength = $form->correctanswerlength[$key];
259             $a->correctanswerformat = $form->correctanswerformat[$key];
260             $question->options->answers[] = clone($a);
261         }
263         return $question;
264     }
266     function convert_answers (&$question, &$state){
267         foreach ($question->options->answers as $key => $answer) {
268             $answer->answer = $this->substitute_variables($answer->answer, $state->options->dataset);
269             //evaluate the equations i.e {=5+4)
270             $qtext = "";
271             $qtextremaining = $answer->answer ;
272             //   while  (preg_match('~\{(=)|%[[:digit]]\.=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
273             while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
275                 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
276                 $qtext = $qtext.$qtextsplits[0];
277                 $qtextremaining = $qtextsplits[1];
278                 if (empty($regs1[1])) {
279                     $str = '';
280                 } else {
281                     if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
282                         $str=$formulaerrors ;
283                     }else {
284                         eval('$str = '.$regs1[1].';');
285                         $texteval= qtype_calculated_calculate_answer(
286                             $str, $state->options->dataset, $answer->tolerance,
287                             $answer->tolerancetype, $answer->correctanswerlength,
288                             $answer->correctanswerformat, '');
289                         $str = $texteval->answer;
290                     }
291                 }
292                 $qtext = $qtext.$str ;
293             }
294             $answer->answer = $qtext.$qtextremaining ; ;
295         }
296     }
298     function get_default_numerical_unit($question, $virtualqtype){
299         $unit = '';
300         return $unit ;
301     }
302     function grade_responses(&$question, &$state, $cmoptions) {
303         // Forward the grading to the virtual qtype
304         // We modify the question to look like a multichoice question
305         // for grading nothing to do
306 /*        $numericalquestion = fullclone($question);
307        foreach ($numericalquestion->options->answers as $key => $answer) {
308             $answer = $numericalquestion->options->answers[$key]->answer; // for PHP 4.x
309           $numericalquestion->options->answers[$key]->answer = $this->substitute_variables_and_eval($answer,
310              $state->options->dataset);
311 }*/
312         $virtualqtype = $this->get_virtual_qtype( $question);
313         return $virtualqtype->grade_responses($question, $state, $cmoptions) ;
314     }
318     // ULPGC ecastro
319     function get_actual_response(&$question, &$state) {
320         // Substitute variables in questiontext before giving the data to the
321         // virtual type
322         $virtualqtype = $this->get_virtual_qtype( $question);
323         $unit = '' ;//$virtualqtype->get_default_numerical_unit($question);
325         // We modify the question to look like a multichoice question
326         $numericalquestion = clone($question);
327         $this->convert_answers ($numericalquestion, $state);
328         $this->convert_questiontext ($numericalquestion, $state);
329      /*   $numericalquestion->questiontext = $this->substitute_variables_and_eval(
330      $numericalquestion->questiontext, $state->options->dataset);*/
331         $responses = $virtualqtype->get_all_responses($numericalquestion, $state);
332         $response = reset($responses->responses);
333         $correct = $response->answer.' : ';
335         $responses = $virtualqtype->get_actual_response($numericalquestion, $state);
337         foreach ($responses as $key=>$response){
338             $responses[$key] = $correct.$response;
339         }
341         return $responses;
342     }
344     function create_virtual_qtype() {
345         global $CFG;
346         require_once("$CFG->dirroot/question/type/multichoice/questiontype.php");
347         return new question_multichoice_qtype();
348     }
351     function comment_header($question) {
352         //$this->get_question_options($question);
353         $strheader = '';
354         $delimiter = '';
356         $answers = $question->options->answers;
358         foreach ($answers as $key => $answer) {
359             if (is_string($answer)) {
360                 $strheader .= $delimiter.$answer;
361             } else {
362                 $strheader .= $delimiter.$answer->answer;
363             }
364             $delimiter = '<br/>';
365         }
366         return $strheader;
367     }
369     function comment_on_datasetitems($qtypeobj,$questionid,$questiontext, $answers,$data, $number) { //multichoice_
370         global $DB;
371         $comment = new stdClass;
372         $comment->stranswers = array();
373         $comment->outsidelimit = false ;
374         $comment->answers = array();
375         /// Find a default unit:
376     /*    if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units', array('question'=> $questionid, 'multiplier' => 1.0))) {
377             $unit = $unit->unit;
378         } else {
379             $unit = '';
380     }*/
382         $answers = fullclone($answers);
383         $strmin = get_string('min', 'quiz');
384         $strmax = get_string('max', 'quiz');
385         $errors = '';
386         $delimiter = ': ';
387         foreach ($answers as $key => $answer) {
388             $answer->answer = $this->substitute_variables($answer->answer, $data);
389             //evaluate the equations i.e {=5+4)
390             $qtext = "";
391             $qtextremaining = $answer->answer ;
392             while  (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
393                 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
394                 $qtext =$qtext.$qtextsplits[0];
395                 $qtextremaining = $qtextsplits[1];
396                 if (empty($regs1[1])) {
397                     $str = '';
398                 } else {
399                     if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
400                         $str=$formulaerrors ;
401                     }else {
402                         eval('$str = '.$regs1[1].';');
403                     }
404                 }
405                 $qtext = $qtext.$str ;
406             }
407             $answer->answer = $qtext.$qtextremaining;
408             $comment->stranswers[$key] = $answer->answer;
411           /*  $formula = $this->substitute_variables($answer->answer,$data);
412             $formattedanswer = qtype_calculated_calculate_answer(
413                     $answer->answer, $data, $answer->tolerance,
414                     $answer->tolerancetype, $answer->correctanswerlength,
415                     $answer->correctanswerformat, $unit);
416                     if ( $formula === '*'){
417                         $answer->min = ' ';
418                         $formattedanswer->answer = $answer->answer ;
419                     }else {
420                         eval('$answer->answer = '.$formula.';') ;
421                         $virtualqtype->get_tolerance_interval($answer);
422                     }
423             if ($answer->min === '') {
424                 // This should mean that something is wrong
425                 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
426             } else if ($formula === '*'){
427                 $comment->stranswers[$key] = $formula.' = '.get_string('anyvalue','qtype_calculated').'<br/><br/><br/>';
428             }else{
429                 $comment->stranswers[$key]= $formula.' = '.$formattedanswer->answer.'<br/>' ;
430                 $comment->stranswers[$key] .= $strmin. $delimiter.$answer->min.'---';
431                 $comment->stranswers[$key] .= $strmax.$delimiter.$answer->max;
432                 $comment->stranswers[$key] .='<br/>';
433                 $correcttrue->correct = $formattedanswer->answer ;
434                 $correcttrue->true = $answer->answer ;
435                 if ($formattedanswer->answer < $answer->min || $formattedanswer->answer > $answer->max){
436                     $comment->outsidelimit = true ;
437                     $comment->answers[$key] = $key;
438                     $comment->stranswers[$key] .=get_string('trueansweroutsidelimits','qtype_calculated',$correcttrue);//<span class="error">ERROR True answer '..' outside limits</span>';
439                 }else {
440                     $comment->stranswers[$key] .=get_string('trueanswerinsidelimits','qtype_calculated',$correcttrue);//' True answer :'.$calculated->trueanswer.' inside limits';
441                 }
442                 $comment->stranswers[$key] .='';
443           }*/
444         }
445         return fullclone($comment);
446     }
448     function get_correct_responses1(&$question, &$state) {
449         $virtualqtype = $this->get_virtual_qtype( $question);
450     /*    if ($question->options->multichoice != 1 ) {
451             if($unit = $virtualqtype->get_default_numerical_unit($question)){
452                  $unit = $unit->unit;
453             } else {
454                 $unit = '';
455             }
456             foreach ($question->options->answers as $answer) {
457                 if (((int) $answer->fraction) === 1) {
458                     $answernumerical = qtype_calculated_calculate_answer(
459                      $answer->answer, $state->options->dataset, $answer->tolerance,
460                      $answer->tolerancetype, $answer->correctanswerlength,
461                         $answer->correctanswerformat, ''); // remove unit
462                         $correct = array('' => $answernumerical->answer);
463                         $correct['answer']= $correct[''];
464                     if (isset($correct['']) && $correct[''] != '*' && $unit ) {
465                             $correct[''] .= ' '.$unit;
466                             $correct['unit']= $unit;
467                     }
468                     return $correct;
469                 }
470             }
471     }else{**/
472         return $virtualqtype->get_correct_responses($question, $state) ;
473         // }
474         return null;
475     }
477     function get_virtual_qtype() {
478         global $QTYPES;
479         //    if ( isset($question->options->multichoice) && $question->options->multichoice == '1'){
480         $this->virtualqtype =& $QTYPES['multichoice'];
481         //   }else {
482         //       $this->virtualqtype =& $QTYPES['numerical'];
483         //   }
484         return $this->virtualqtype;
485     }
488     /**
489      * Runs all the code required to set up and save an essay question for testing purposes.
490      * Alternate DB table prefix may be used to facilitate data deletion.
491      */
492     function generate_test($name, $courseid = null) {
493         global $DB;
494         list($form, $question) = parent::generate_test($name, $courseid);
495         $form->feedback = 1;
496         $form->multiplier = array(1, 1);
497         $form->shuffleanswers = 1;
498         $form->noanswers = 1;
499         $form->qtype ='calculatedmulti';
500         $question->qtype ='calculatedmulti';
501         $form->answers = array('{a} + {b}');
502         $form->fraction = array(1);
503         $form->tolerance = array(0.01);
504         $form->tolerancetype = array(1);
505         $form->correctanswerlength = array(2);
506         $form->correctanswerformat = array(1);
507         $form->questiontext = "What is {a} + {b}?";
509         if ($courseid) {
510             $course = $DB->get_record('course', array('id'=> $courseid));
511         }
513         $new_question = $this->save_question($question, $form, $course);
515         $dataset_form = new stdClass();
516         $dataset_form->nextpageparam["forceregeneration"]= 1;
517         $dataset_form->calcmin = array(1 => 1.0, 2 => 1.0);
518         $dataset_form->calcmax = array(1 => 10.0, 2 => 10.0);
519         $dataset_form->calclength = array(1 => 1, 2 => 1);
520         $dataset_form->number = array(1 => 5.4 , 2 => 4.9);
521         $dataset_form->itemid = array(1 => '' , 2 => '');
522         $dataset_form->calcdistribution = array(1 => 'uniform', 2 => 'uniform');
523         $dataset_form->definition = array(1 => "1-0-a",
524             2 => "1-0-b");
525         $dataset_form->nextpageparam = array('forceregeneration' => false);
526         $dataset_form->addbutton = 1;
527         $dataset_form->selectadd = 1;
528         $dataset_form->courseid = $courseid;
529         $dataset_form->cmid = 0;
530         $dataset_form->id = $new_question->id;
531         $this->save_dataset_items($new_question, $dataset_form);
533         return $new_question;
534     }
536     /**
537      * When move the category of questions, the belonging files should be moved as well
538      * @param object $question, question information
539      * @param object $newcategory, target category information
540      */
541     function move_files($question, $newcategory) {
542         global $DB;
543         // move files belonging to question component
544         parent::move_files($question, $newcategory);
546         // move files belonging to qtype_calculatedmulti
547         $fs = get_file_storage();
548         // process files in answer
549         if (!$oldanswers = $DB->get_records('question_answers', array('question' =>  $question->id), 'id ASC')) {
550             $oldanswers = array();
551         }
552         $component = 'question';
553         $filearea = 'answerfeedback';
554         foreach ($oldanswers as $answer) {
555             $files = $fs->get_area_files($question->contextid, $component, $filearea, $answer->id);
556             foreach ($files as $storedfile) {
557                 if (!$storedfile->is_directory()) {
558                     $newfile = new object();
559                     $newfile->contextid = (int)$newcategory->contextid;
560                     $fs->create_file_from_storedfile($newfile, $storedfile);
561                     $storedfile->delete();
562                 }
563             }
564         }
566         $component = 'qtype_calculatedmulti';
567         foreach (array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback') as $filearea) {
568             $files = $fs->get_area_files($question->contextid, $component, $filearea, $question->id);
569             foreach ($files as $storedfile) {
570                 if (!$storedfile->is_directory()) {
571                     $newfile = new object();
572                     $newfile->contextid = (int)$newcategory->contextid;
573                     $fs->create_file_from_storedfile($newfile, $storedfile);
574                     $storedfile->delete();
575                 }
576             }
577         }
578     }
580     function check_file_access($question, $state, $options, $contextid, $component,
581             $filearea, $args) {
582         $itemid = reset($args);
584         if (empty($question->maxgrade)) {
585             $question->maxgrade = $question->defaultgrade;
586         }
588         if (in_array($filearea, array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) {
589             $result = $options->feedback && ($itemid == $question->id);
590             if (!$result) {
591                 return false;
592             }
593             if ($state->raw_grade >= $question->maxgrade/1.01) {
594                 $feedbacktype = 'correctfeedback';
595             } else if ($state->raw_grade > 0) {
596                 $feedbacktype = 'partiallycorrectfeedback';
597             } else {
598                 $feedbacktype = 'incorrectfeedback';
599             }
600             if ($feedbacktype != $filearea) {
601                 return false;
602             }
603             return true;
604         } else if ($component == 'question' && $filearea == 'answerfeedback') {
605             return $options->feedback && (array_key_exists($itemid, $question->options->answers));
606         } else {
607             return parent::check_file_access($question, $state, $options, $contextid, $component,
608                     $filearea, $args);
609         }
610     }
613 //// END OF CLASS ////
615 //////////////////////////////////////////////////////////////////////////
616 //// INITIATION - Without this line the question type is not in use... ///
617 //////////////////////////////////////////////////////////////////////////
618 question_register_questiontype(new question_calculatedmulti_qtype());
620 if ( ! defined ("CALCULATEDMULTI")) {
621     define("CALCULATEDMULTI",    "calculatedmulti");