13f9f522c603ac776a26d75c2b3b757f9185374d
[moodle.git] / question / type / calculated / 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/>.
19 /////////////////
20 // CALCULATED ///
21 /////////////////
23 /// QUESTION TYPE CLASS //////////////////
25 class question_calculated_qtype extends default_questiontype {
27     // Used by the function custom_generator_tools:
28     public $calcgenerateidhasbeenadded = false;
29     public $virtualqtype = false;
30     public $wizard_pages_number = 3 ;
32     function name() {
33         return 'calculated';
34     }
36     function has_wildcards_in_responses($question, $subqid) {
37         return true;
38     }
40     function requires_qtypes() {
41         return array('numerical');
42     }
44     function get_question_options(&$question) {
45         // First get the datasets and default options
46         // the code is used for calculated, calculatedsimple and calculatedmulti qtypes
47         global $CFG, $DB, $OUTPUT, $QTYPES;
48         if (!$question->options = $DB->get_record('question_calculated_options', array('question' => $question->id))) {
49             //  echo $OUTPUT->notification('Error: Missing question options for calculated question'.$question->id.'!');
50             //  return false;
51             $question->options->synchronize = 0;
52             $question->options->single = 0; //$question->single;
53             $question->options->answernumbering = 'abc';
54             $question->options->shuffleanswers = 0 ;
55             $question->options->correctfeedback = '';
56             $question->options->partiallycorrectfeedback = '';
57             $question->options->incorrectfeedback = '';
58         }
60         if (!$question->options->answers = $DB->get_records_sql(
61             "SELECT a.*, c.tolerance, c.tolerancetype, c.correctanswerlength, c.correctanswerformat " .
62             "FROM {question_answers} a, " .
63             "     {question_calculated} c " .
64             "WHERE a.question = ? " .
65             "AND   a.id = c.answer ".
66             "ORDER BY a.id ASC", array($question->id))) {
67                 echo $OUTPUT->notification('Error: Missing question answer for calculated question ' . $question->id . '!');
68                 return false;
69             }
71         if ( $this->get_virtual_qtype() ==  $QTYPES['numerical']){
72             $QTYPES['numerical']->get_numerical_units($question);
73             $QTYPES['numerical']->get_numerical_options($question);
74         }
76         if( isset($question->export_process)&&$question->export_process){
77             $question->options->datasets = $this->get_datasets_for_export($question);
78         }
79         return true;
80     }
82     function get_datasets_for_export(&$question){
83         global $DB, $CFG;
84         $datasetdefs = array();
85         if (!empty($question->id)) {
86             $sql = "SELECT i.*
87                       FROM {question_datasets} d, {question_dataset_definitions} i
88                      WHERE d.question = ? AND d.datasetdefinition = i.id";
89             if ($records = $DB->get_records_sql($sql, array($question->id))) {
90                 foreach ($records as $r) {
91                     $def = $r ;
92                     if ($def->category=='0'){
93                         $def->status='private';
94                     } else {
95                         $def->status='shared';
96                     }
97                     $def->type ='calculated' ;
98                     list($distribution, $min, $max,$dec) = explode(':', $def->options, 4);
99                     $def->distribution=$distribution;
100                     $def->minimum=$min;
101                     $def->maximum=$max;
102                     $def->decimals=$dec ;
103                     if ($def->itemcount > 0 ) {
104                         // get the datasetitems
105                         $def->items = array();
106                         if ($items = $this->get_database_dataset_items($def->id)){
107                             $n = 0;
108                             foreach( $items as $ii){
109                                 $n++;
110                                 $def->items[$n] = new stdClass;
111                                 $def->items[$n]->itemnumber=$ii->itemnumber;
112                                 $def->items[$n]->value=$ii->value;
113                             }
114                             $def->number_of_items=$n ;
115                         }
116                     }
117                     $datasetdefs["1-$r->category-$r->name"] = $def;
118                 }
119             }
120         }
121         return $datasetdefs ;
122     }
124     function save_question_options($question) {
125         global $CFG, $DB, $QTYPES ;
126         // the code is used for calculated, calculatedsimple and calculatedmulti qtypes
127         $context = $question->context;
128         if (isset($question->answer) && !isset($question->answers)) {
129             $question->answers = $question->answer;
130         }
131         // calculated options
132         $update = true ;
133         $options = $DB->get_record("question_calculated_options", array("question" => $question->id));
134         if (!$options) {
135             $update = false;
136             $options = new stdClass;
137             $options->question = $question->id;
138         }
139         // as used only by calculated
140         if(isset($question->synchronize)){
141             $options->synchronize = $question->synchronize;
142         }else {
143             $options->synchronize = 0 ;
144         }
145         $options->single = 0; //$question->single;
146         $options->answernumbering =  $question->answernumbering;
147         $options->shuffleanswers = $question->shuffleanswers;
149         foreach (array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback') as $feedbackname) {
150             $feedback = $question->$feedbackname;
151             $options->$feedbackname = trim($feedback['text']);
152             $feedbackformat = $feedbackname . 'format';
153             $options->$feedbackformat = trim($feedback['format']);
154             $options->$feedbackname = file_save_draft_area_files($feedback['itemid'], $context->id, 'qtype_calculated', $feedbackname, $question->id, self::$fileoptions, trim($feedback['text']));
155         }
157         if ($update) {
158             $DB->update_record("question_calculated_options", $options);
159         } else {
160             $DB->insert_record("question_calculated_options", $options);
161         }
163         // Get old versions of the objects
164         if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) {
165             $oldanswers = array();
166         }
168         if (!$oldoptions = $DB->get_records('question_calculated', array('question' => $question->id), 'answer ASC')) {
169             $oldoptions = array();
170         }
172         // Save the units.
173         $virtualqtype = $this->get_virtual_qtype();
174         $result = $virtualqtype->save_numerical_units($question);
175         if (isset($result->error)) {
176             return $result;
177         } else {
178             $units = &$result->units;
179         }
180         // Insert all the new answers
181         if (isset($question->answer) && !isset($question->answers)) {
182             $question->answers=$question->answer;
183         }
184         foreach ($question->answers as $key => $dataanswer) {
185             if ( trim($dataanswer) != '' ) {
186                 $answer = new stdClass;
187                 $answer->question = $question->id;
188                 $answer->answer = trim($dataanswer);
189                 $answer->fraction = $question->fraction[$key];
190                 $answer->feedbackformat = $question->feedback[$key]['format'];
192                 if ($oldanswer = array_shift($oldanswers)) {  // Existing answer, so reuse it
193                     $answer->id = $oldanswer->id;
194                     $answer->feedback = file_save_draft_area_files($question->feedback[$key]['itemid'], $context->id, 'question', 'answerfeedback', $oldanswer->id, self::$fileoptions, trim($question->feedback[$key]['text']));
195                     $DB->update_record("question_answers", $answer);
196                 } else { // This is a completely new answer
197                     $answer->feedback = '';
198                     $answer->id = $DB->insert_record("question_answers", $answer);
199                     $answer->feedback = file_save_draft_area_files($question->feedback[$key]['itemid'], $context->id, 'question', 'answerfeedback', $answer->id, self::$fileoptions, trim($question->feedback[$key]['text']));
200                     $DB->set_field('question_answers', 'feedback', $answer->feedback, array('id'=>$answer->id));
201                 }
203                 // Set up the options object
204                 if (!$options = array_shift($oldoptions)) {
205                     $options = new stdClass;
206                 }
207                 $options->question  = $question->id;
208                 $options->answer    = $answer->id;
209                 $options->tolerance = trim($question->tolerance[$key]);
210                 $options->tolerancetype  = trim($question->tolerancetype[$key]);
211                 $options->correctanswerlength  = trim($question->correctanswerlength[$key]);
212                 $options->correctanswerformat  = trim($question->correctanswerformat[$key]);
214                 // Save options
215                 if (isset($options->id)) { // reusing existing record
216                     $DB->update_record('question_calculated', $options);
217                 } else { // new options
218                     $DB->insert_record('question_calculated', $options);
219                 }
220             }
221         }
222         // delete old answer records
223         if (!empty($oldanswers)) {
224             foreach($oldanswers as $oa) {
225                 $DB->delete_records('question_answers', array('id' => $oa->id));
226             }
227         }
229         // delete old answer records
230         if (!empty($oldoptions)) {
231             foreach($oldoptions as $oo) {
232                 $DB->delete_records('question_calculated', array('id' => $oo->id));
233             }
234         }
236         $result = $QTYPES['numerical']->save_numerical_options($question);
237         if (isset($result->error)) {
238             return $result;
239         }
241         if( isset($question->import_process)&&$question->import_process){
242             $this->import_datasets($question);
243         }
244         // Report any problems.
245         if (!empty($result->notice)) {
246             return $result;
247         }
248         return true;
249     }
251     function import_datasets($question){
252         global $DB;
253         $n = count($question->dataset);
254         foreach ($question->dataset as $dataset) {
255             // name, type, option,
256             $datasetdef = new stdClass();
257             $datasetdef->name = $dataset->name;
258             $datasetdef->type = 1 ;
259             $datasetdef->options =  $dataset->distribution.':'.$dataset->min.':'.$dataset->max.':'.$dataset->length;
260             $datasetdef->itemcount=$dataset->itemcount;
261             if ( $dataset->status =='private'){
262                 $datasetdef->category = 0;
263                 $todo='create' ;
264             }else if ($dataset->status =='shared' ){
265                 if ($sharedatasetdefs = $DB->get_records_select(
266                     'question_dataset_definitions',
267                     "type = '1'
268                     AND name = ?
269                     AND category = ?
270                     ORDER BY id DESC ", array($dataset->name, $question->category)
271                 )) { // so there is at least one
272                     $sharedatasetdef = array_shift($sharedatasetdefs);
273                     if ( $sharedatasetdef->options ==  $datasetdef->options ){// identical so use it
274                         $todo='useit' ;
275                         $datasetdef =$sharedatasetdef ;
276                     } else { // different so create a private one
277                         $datasetdef->category = 0;
278                         $todo='create' ;
279                     }
280                 }else { // no so create one
281                     $datasetdef->category =$question->category ;
282                     $todo='create' ;
283                 }
284             }
285             if (  $todo=='create'){
286                 $datasetdef->id = $DB->insert_record( 'question_dataset_definitions', $datasetdef);
287             }
288             // Create relation to the dataset:
289             $questiondataset = new stdClass;
290             $questiondataset->question = $question->id;
291             $questiondataset->datasetdefinition = $datasetdef->id;
292             $DB->insert_record('question_datasets', $questiondataset);
293             if ($todo=='create'){ // add the items
294                 foreach ($dataset->datasetitem as $dataitem ){
295                     $datasetitem = new stdClass;
296                     $datasetitem->definition=$datasetdef->id ;
297                     $datasetitem->itemnumber = $dataitem->itemnumber ;
298                     $datasetitem->value = $dataitem->value ;
299                     $DB->insert_record('question_dataset_items', $datasetitem);
300                 }
301             }
302         }
303     }
305     function restore_session_and_responses(&$question, &$state) {
306         global $OUTPUT;
307         if (!preg_match('~^dataset([0-9]+)[^-]*-(.*)$~',
308             $state->responses[''], $regs)) {
309                 echo $OUTPUT->notification("Wrongly formatted raw response answer " .
310                     "{$state->responses['']}! Could not restore session for " .
311                     " question #{$question->id}.");
312                 $state->options->datasetitem = 1;
313                 $state->options->dataset = array();
314                 $state->responses = array('' => '');
315                 return false;
316             }
318         // Restore the chosen dataset
319         $state->options->datasetitem = $regs[1];
320         $state->options->dataset =
321             $this->pick_question_dataset($question,$state->options->datasetitem);
322         $state->responses = array('' => $regs[2]);
323         $virtualqtype = $this->get_virtual_qtype();
324         return $virtualqtype->restore_session_and_responses($question, $state);
325     }
327     function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
328         // Find out how many datasets are available
329         global $CFG, $DB, $QTYPES, $OUTPUT;
330         if(!$maxnumber = (int)$DB->get_field_sql(
331             "SELECT MIN(a.itemcount)
332                FROM {question_dataset_definitions} a, {question_datasets} b
333               WHERE b.question = ? AND a.id = b.datasetdefinition", array($question->id))) {
334             print_error('cannotgetdsforquestion', 'question', '', $question->id);
335         }
337         $sql = "SELECT i.*
338                   FROM {question_datasets} d, {question_dataset_definitions} i
339                  WHERE d.question = ? AND d.datasetdefinition = i.id AND i.category != 0";
341         if (!$question->options->synchronize || !$records = $DB->get_records_sql($sql, array($question->id))) {
342             $synchronize_calculated  =  false ;
343         }else {
344             // i.e records is true so test coherence
345             $coherence = true ;
346             $a = new stdClass ;
347             $a->qid = $question->id ;
348             $a->qcat = $question->category ;
349             foreach($records as $def ){
350                 if ($def->category != $question->category){
351                     $a->name = $def->name;
352                     $a->sharedcat = $def->category ;
353                     $coherence = false ;
354                     break;
355                 }
356             }
357             if(!$coherence){
358                 echo $OUTPUT->notification(get_string('nocoherencequestionsdatyasetcategory','qtype_calculated',$a));
359             }
360             $synchronize_calculated  = true ;
361         }
363         // Choose a random dataset
364         // maxnumber sould not be breater than 100
365         if ($maxnumber > CALCULATEDQUESTIONMAXITEMNUMBER ){
366             $maxnumber = CALCULATEDQUESTIONMAXITEMNUMBER ;
367         }
368         if ( $synchronize_calculated === false ) {
369             $state->options->datasetitem = rand(1, $maxnumber);
370         }else{
371             $state->options->datasetitem = intval( $maxnumber * substr($attempt->timestart,-2) /100 ) ;
372             if ($state->options->datasetitem < 1) {
373                 $state->options->datasetitem =1 ;
374             } else if ($state->options->datasetitem > $maxnumber){
375                 $state->options->datasetitem = $maxnumber ;
376             }
378         };
379         $state->options->dataset =
380             $this->pick_question_dataset($question,$state->options->datasetitem);
381         $virtualqtype = $this->get_virtual_qtype( );
382         return $virtualqtype->create_session_and_responses($question, $state, $cmoptions, $attempt);
383     }
385     function save_session_and_responses(&$question, &$state) {
386         global $DB;
387         $responses = 'dataset'.$state->options->datasetitem.'-' ;
388         // regular numeric type
389         if(isset($state->responses['unit']) && isset($question->options->units[$state->responses['unit']])){
390             $responses .= $state->responses['answer'].'|||||'.$question->options->units[$state->responses['unit']]->unit;
391         }else if(isset($state->responses['unit'])){
392             $responses .= $state->responses['answer'].'|||||'.$state->responses['unit'] ;
393         }else {
394             $responses .= $state->responses['answer'].'|||||';
395         }
397         // Set the legacy answer field
398         $DB->set_field('question_states', 'answer', $responses, array('id'=> $state->id));
400         return true;
401     }
403     function create_runtime_question($question, $form) {
404         $question = parent::create_runtime_question($question, $form);
405         $question->options->answers = array();
406         foreach ($form->answers as $key => $answer) {
407             $a->answer              = trim($form->answer[$key]);
408             $a->fraction              = $form->fraction[$key];//new
409             $a->tolerance           = $form->tolerance[$key];
410             $a->tolerancetype       = $form->tolerancetype[$key];
411             $a->correctanswerlength = $form->correctanswerlength[$key];
412             $a->correctanswerformat = $form->correctanswerformat[$key];
413             $question->options->answers[] = clone($a);
414         }
416         return $question;
417     }
419     function validate_form($form) {
420         switch($form->wizardpage) {
421         case 'question':
422             $calculatedmessages = array();
423             if (empty($form->name)) {
424                 $calculatedmessages[] = get_string('missingname', 'quiz');
425             }
426             if (empty($form->questiontext)) {
427                 $calculatedmessages[] = get_string('missingquestiontext', 'quiz');
428             }
429             // Verify formulas
430             foreach ($form->answers as $key => $answer) {
431                 if ('' === trim($answer)) {
432                     $calculatedmessages[] =
433                         get_string('missingformula', 'quiz');
434                 }
435                 if ($formulaerrors =
436                     qtype_calculated_find_formula_errors($answer)) {
437                         $calculatedmessages[] = $formulaerrors;
438                     }
439                 if (! isset($form->tolerance[$key])) {
440                     $form->tolerance[$key] = 0.0;
441                 }
442                 if (! is_numeric($form->tolerance[$key])) {
443                     $calculatedmessages[] =
444                         get_string('tolerancemustbenumeric', 'quiz');
445                 }
446             }
448             if (!empty($calculatedmessages)) {
449                 $errorstring = "The following errors were found:<br />";
450                 foreach ($calculatedmessages as $msg) {
451                     $errorstring .= $msg . '<br />';
452                 }
453                 print_error($errorstring);
454             }
456             break;
457         default:
458             return parent::validate_form($form);
459             break;
460         }
461         return true;
462     }
463     function finished_edit_wizard(&$form) {
464         return isset($form->backtoquiz);
465     }
466     function wizard_pages_number() {
467         return 3 ;
468     }
469     // This gets called by editquestion.php after the standard question is saved
470     function print_next_wizard_page(&$question, &$form, $course) {
471         global $CFG, $USER, $SESSION, $COURSE;
473         // Catch invalid navigation & reloads
474         if (empty($question->id) && empty($SESSION->calculated)) {
475             redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired.', 3);
476         }
478         // See where we're coming from
479         switch($form->wizardpage) {
480         case 'question':
481             require("$CFG->dirroot/question/type/calculated/datasetdefinitions.php");
482             break;
483         case 'datasetdefinitions':
484         case 'datasetitems':
485             require("$CFG->dirroot/question/type/calculated/datasetitems.php");
486             break;
487         default:
488             print_error('invalidwizardpage', 'question');
489             break;
490         }
491     }
493     // This gets called by question2.php after the standard question is saved
494     function &next_wizard_form($submiturl, $question, $wizardnow){
495         global $CFG, $SESSION, $COURSE;
497         // Catch invalid navigation & reloads
498         if (empty($question->id) && empty($SESSION->calculated)) {
499             redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired. Cannot get next wizard form.', 3);
500         }
501         if (empty($question->id)){
502             $question =& $SESSION->calculated->questionform;
503         }
505         // See where we're coming from
506         switch($wizardnow) {
507         case 'datasetdefinitions':
508             require("$CFG->dirroot/question/type/calculated/datasetdefinitions_form.php");
509             $mform = new question_dataset_dependent_definitions_form("$submiturl?wizardnow=datasetdefinitions", $question);
510             break;
511         case 'datasetitems':
512             require("$CFG->dirroot/question/type/calculated/datasetitems_form.php");
513             $regenerate = optional_param('forceregeneration', 0, PARAM_BOOL);
514             $mform = new question_dataset_dependent_items_form("$submiturl?wizardnow=datasetitems", $question, $regenerate);
515             break;
516         default:
517             print_error('invalidwizardpage', 'question');
518             break;
519         }
521         return $mform;
522     }
524     /**
525      * This method should be overriden if you want to include a special heading or some other
526      * html on a question editing page besides the question editing form.
527      *
528      * @param question_edit_form $mform a child of question_edit_form
529      * @param object $question
530      * @param string $wizardnow is '' for first page.
531      */
532     function display_question_editing_page(&$mform, $question, $wizardnow){
533         global $OUTPUT ;
534         switch ($wizardnow){
535         case '':
536             //on first page default display is fine
537             parent::display_question_editing_page($mform, $question, $wizardnow);
538             return;
539             break;
540         case 'datasetdefinitions':
541             echo $OUTPUT->heading_with_help(get_string("choosedatasetproperties", "qtype_calculated"), 'questiondatasets', 'qtype_calculated');
542             break;
543         case 'datasetitems':
544             echo $OUTPUT->heading_with_help(get_string("editdatasets", "qtype_calculated"), 'questiondatasets', 'qtype_calculated');
545             break;
546         }
548         $mform->display();
549     }
551     /**
552      * This method prepare the $datasets in a format similar to dadatesetdefinitions_form.php
553      * so that they can be saved
554      * using the function save_dataset_definitions($form)
555      *  when creating a new calculated question or
556      *  whenediting an already existing calculated question
557      * or by  function save_as_new_dataset_definitions($form, $initialid)
558      *  when saving as new an already existing calculated question
559      *
560      * @param object $form
561      * @param int $questionfromid default = '0'
562      */
563     function preparedatasets(&$form , $questionfromid='0'){
564         // the dataset names present in the edit_question_form and edit_calculated_form are retrieved
565         $possibledatasets = $this->find_dataset_names($form->questiontext);
566         $mandatorydatasets = array();
567         foreach ($form->answers as $answer) {
568             $mandatorydatasets += $this->find_dataset_names($answer);
569         }
570         // if there are identical datasetdefs already saved in the original question.
571         // either when editing a question or saving as new
572         // they are retrieved using $questionfromid
573         if ($questionfromid!='0'){
574             $form->id = $questionfromid ;
575         }
576         $datasets = array();
577         $key = 0 ;
578         // always prepare the mandatorydatasets present in the answers
579         // the $options are not used here
580         foreach ($mandatorydatasets as $datasetname) {
581             if (!isset($datasets[$datasetname])) {
582                 list($options, $selected) =
583                     $this->dataset_options($form, $datasetname);
584                 $datasets[$datasetname]='';
585                 $form->dataset[$key]=$selected ;
586                 $key++;
587             }
588         }
589         // do not prepare possibledatasets when creating a question
590         // they will defined and stored with datasetdefinitions_form.php
591         // the $options are not used here
592         if ($questionfromid!='0'){
594             foreach ($possibledatasets as $datasetname) {
595                 if (!isset($datasets[$datasetname])) {
596                     list($options, $selected) =
597                         $this->dataset_options($form, $datasetname,false);
598                     $datasets[$datasetname]='';
599                     $form->dataset[$key]=$selected ;
600                     $key++;
601                 }
602             }
603         }
604         return $datasets ;
605     }
606     function addnamecategory(&$question){
607         global $DB;
608         $categorydatasetdefs = $DB->get_records_sql(
609             "SELECT  a.*
610                FROM {question_datasets} b, {question_dataset_definitions} a
611               WHERE a.id = b.datasetdefinition AND a.type = '1' AND a.category != 0 AND b.question = ?
612            ORDER BY a.name ", array($question->id));
613         $questionname = $question->name ;
614         $regs= array();
615         if(preg_match('~#\{([^[:space:]]*)#~',$questionname , $regs)){
616             $questionname = str_replace($regs[0], '', $questionname);
617         };
619         if (!empty($categorydatasetdefs)){ // there is at least one with the same name
620             $questionname  ="#".$questionname;
621             foreach($categorydatasetdefs as $def) {
622                 if(strlen("{$def->name}")+strlen($questionname) < 250 ){
623                     $questionname = '{'.$def->name.'}'
624                         .$questionname;
625                 }
626             }
627             $questionname ="#".$questionname;
628         }
629         $DB->set_field('question', 'name', $questionname, array("id" => $question->id));
630     }
632     /**
633      * this version save the available data at the different steps of the question editing process
634      * without using global $SESSION as storage between steps
635      * at the first step $wizardnow = 'question'
636      *  when creating a new question
637      *  when modifying a question
638      *  when copying as a new question
639      *  the general parameters and answers are saved using parent::save_question
640      *  then the datasets are prepared and saved
641      * at the second step $wizardnow = 'datasetdefinitions'
642      *  the datadefs final type are defined as private, category or not a datadef
643      * at the third step $wizardnow = 'datasetitems'
644      *  the datadefs parameters and the data items are created or defined
645      *
646      * @param object question
647      * @param object $form
648      * @param int $course
649      * @param PARAM_ALPHA $wizardnow should be added as we are coming from question2.php
650      */
651     function save_question($question, $form, $course) {
652         global $DB;
653         if ($this->wizard_pages_number() == 1 ){
654                 $question = parent::save_question($question, $form, $course);
655             return $question ;
656         }
658         $wizardnow =  optional_param('wizardnow', '', PARAM_ALPHA);
659         $id = optional_param('id', 0, PARAM_INT); // question id
660         // in case 'question'
661         // for a new question $form->id is empty
662         // when saving as new question
663         //   $question->id = 0, $form is $data from question2.php
664         //   and $data->makecopy is defined as $data->id is the initial question id
665         // edit case. If it is a new question we don't necessarily need to
666         // return a valid question object
668         // See where we're coming from
669         switch($wizardnow) {
670         case '' :
671         case 'question': // coming from the first page, creating the second
672             if (empty($form->id)) { // for a new question $form->id is empty
673                 $question = parent::save_question($question, $form, $course);
674                 //prepare the datasets using default $questionfromid
675                 $this->preparedatasets($form);
676                 $form->id = $question->id;
677                 $this->save_dataset_definitions($form);
678                 if(isset($form->synchronize) && $form->synchronize == 2 ){
679                     $this->addnamecategory($question);
680                 }
681             } else if (!empty($form->makecopy)){
682                 $questionfromid =  $form->id ;
683                 $question = parent::save_question($question, $form, $course);
684                 //prepare the datasets
685                 $this->preparedatasets($form,$questionfromid);
686                 $form->id = $question->id;
687                 $this->save_as_new_dataset_definitions($form,$questionfromid );
688                 if(isset($form->synchronize) && $form->synchronize == 2 ){
689                     $this->addnamecategory($question);
690                 }
691             }  else {// editing a question
692                 $question = parent::save_question($question, $form, $course);
693                 //prepare the datasets
694                 $this->preparedatasets($form,$question->id);
695                 $form->id = $question->id;
696                 $this->save_dataset_definitions($form);
697                 if(isset($form->synchronize) && $form->synchronize == 2 ){
698                     $this->addnamecategory($question);
699                 }
700             }
701             break;
702         case 'datasetdefinitions':
703             // calculated options
704             // it cannot go here without having done the first page
705             // so the question_calculated_options should exist
706             // only need to update the synchronize field
707             if(isset($form->synchronize) ){
708                 $options_synchronize = $form->synchronize ;
709             }else {
710                 $options_synchronize = 0 ;
711             }
712             $DB->set_field('question_calculated_options', 'synchronize', $options_synchronize, array("question" => $question->id));
713             if(isset($form->synchronize) && $form->synchronize == 2 ){
714                 $this->addnamecategory($question);
715             }
717             $this->save_dataset_definitions($form);
718             break;
719         case 'datasetitems':
720             $this->save_dataset_items($question, $form);
721             $this->save_question_calculated($question, $form);
722             break;
723         default:
724             print_error('invalidwizardpage', 'question');
725             break;
726         }
727         return $question;
728     }
729     /**
730      * Deletes question from the question-type specific tables
731      *
732      * @return boolean Success/Failure
733      * @param object $question  The question being deleted
734      */
735     function delete_question($questionid) {
736         global $DB;
738         $DB->delete_records("question_calculated", array("question" => $questionid));
739         $DB->delete_records("question_calculated_options", array("question" => $questionid));
740         $DB->delete_records("question_numerical_units", array("question" => $questionid));
741         if ($datasets = $DB->get_records('question_datasets', array('question' => $questionid))) {
742             foreach ($datasets as $dataset) {
743                 if (!$DB->get_records_select(
744                     'question_datasets',
745                     "question != ?
746                     AND datasetdefinition = ? ", array($questionid, $dataset->datasetdefinition))){
747                         $DB->delete_records('question_dataset_definitions', array('id' => $dataset->datasetdefinition));
748                         $DB->delete_records('question_dataset_items', array('definition' => $dataset->datasetdefinition));
749                     }
750             }
751         }
752         $DB->delete_records("question_datasets", array("question" => $questionid));
753         return true;
754     }
755     function test_response(&$question, &$state, $answer) {
756         $virtualqtype = $this->get_virtual_qtype();
757         return $virtualqtype->test_response($question, $state, $answer);
759     }
760     function compare_responses(&$question, $state, $teststate) {
762         $virtualqtype = $this->get_virtual_qtype();
763         return $virtualqtype->compare_responses($question, $state, $teststate);
764     }
766     function convert_answers (&$question, &$state){
767         foreach ($question->options->answers as $key => $answer) {
768             $answer = fullclone($question->options->answers[$key]);
769             $question->options->answers[$key]->answer = $this->substitute_variables_and_eval($answer->answer,
770                 $state->options->dataset);
771         }
772     }
773     function convert_questiontext (&$question, &$state){
774         $tolerancemax =0.01;
775         $tolerancetypemax = 1 ;
776         $correctanswerlengthmax = 2 ;
777         $correctanswerformatmax = 1 ;
778         $tolerancemaxset = false ;
779         foreach ($question->options->answers as $key => $answer) {
780             if($answer->fraction == 1.0 && !$tolerancemaxset){
781                 $tolerancemax = $answer->tolerance;
782                 $tolerancetypemax = $answer->tolerancetype ;
783                 $correctanswerlengthmax = $answer->correctanswerlength;
784                 $correctanswerformatmax =$answer->correctanswerformat;
785                 $tolerancemaxset = true ;
786             }
787         }
788         $question->questiontext = $this->substitute_variables($question->questiontext, $state->options->dataset);
789         //evaluate the equations i.e {=5+4)
790         $qtext = "";
791         $qtextremaining = $question->questiontext ;
792         while  (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
793             //  while  (preg_match('~\{=|%=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
794             $qtextsplits = explode($regs1[0], $qtextremaining, 2);
795             $qtext =$qtext.$qtextsplits[0];
796             $qtextremaining = $qtextsplits[1];
797             if (empty($regs1[1])) {
798                 $str = '';
799             } else {
800                 if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
801                     $str=$formulaerrors ;
802                 }else {
803                     eval('$str = '.$regs1[1].';');
804                     $texteval= qtype_calculated_calculate_answer(
805                         $str, $state->options->dataset, $tolerancemax,
806                         $tolerancetypemax, $correctanswerlengthmax,
807                         $correctanswerformatmax, '');
808                     $str = $texteval->answer;
810                     ;
811                 }
812             }
813             $qtext = $qtext.$str ;
814         }
815         $question->questiontext = $qtext.$qtextremaining ; // end replace equations
816     }
818     function get_default_numerical_unit($question,$virtualqtype){
819         if($unit = $virtualqtype->get_default_numerical_unit($question)){
820             $unit = $unit->unit;
821         } else {
822             $unit = '';
823         }
824         return $unit ;
826     }
827     function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
828         // Substitute variables in questiontext before giving the data to the
829         // virtual type for printing
830         $virtualqtype = $this->get_virtual_qtype();
831         // why $unit as it is not use
832         $unit = $this-> get_default_numerical_unit($question,$virtualqtype);
833         // We modify the question to look like a numerical question
834         $numericalquestion = fullclone($question);
835         $this->convert_answers($numericalquestion, $state);
836         $this->convert_questiontext($numericalquestion, $state);
837  /*        $tolerancemax =0.01;
838          $tolerancetypemax = 1 ;
839          $correctanswerlengthmax = 2 ;
840          $correctanswerformatmax = 1 ;
841          $tolerancemaxset = false ;
842         foreach ($numericalquestion->options->answers as $key => $answer) {
843              if($answer->fraction == 1.0 && !$tolerancemaxset){
844                 $tolerancemax = $answer->tolerance;
845                 $tolerancetypemax = $answer->tolerancetype ;
846                 $correctanswerlengthmax = $answer->correctanswerlength;
847                 $correctanswerformatmax =$answer->correctanswerformat;
848                 $tolerancemaxset = true ;
849             }
850         }
852         $numericalquestion->questiontext = $this->substitute_variables(
853         $numericalquestion->questiontext, $state->options->dataset);
854         //evaluate the equations i.e {=5+4)
855         $qtext = "";
856         $qtextremaining = $numericalquestion->questiontext ;
857         while  (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
858       //  while  (preg_match('~\{=|%=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
859             $qtextsplits = explode($regs1[0], $qtextremaining, 2);
860             $qtext =$qtext.$qtextsplits[0];
861             $qtextremaining = $qtextsplits[1];
862             if (empty($regs1[1])) {
863                     $str = '';
864                 } else {
865                     if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
866                         $str=$formulaerrors ;
867                     }else {
868                        eval('$str = '.$regs1[1].';');
869                        $texteval= qtype_calculated_calculate_answer(
870                      $str, $state->options->dataset, $tolerancemax,
871                      $tolerancetypemax, $correctanswerlengthmax,
872                         $correctanswerformatmax, '');
873                         $str = $texteval->answer;
875                         ;
876                     }
877                 }
878                 $qtext = $qtext.$str ;
879         }
880         $numericalquestion->questiontext = $qtext.$qtextremaining ; // end replace equations
881   */
883         $virtualqtype->print_question_formulation_and_controls($numericalquestion, $state, $cmoptions, $options);
884     }
885     function grade_responses(&$question, &$state, $cmoptions) {
886         // Forward the grading to the virtual qtype
887         // We modify the question to look like a numerical question
888         $numericalquestion = fullclone($question);
889         foreach ($numericalquestion->options->answers as $key => $answer) {
890             $answer = $numericalquestion->options->answers[$key]->answer; // for PHP 4.x
891             $numericalquestion->options->answers[$key]->answer = $this->substitute_variables_and_eval($answer,
892                 $state->options->dataset);
893         }
894         $virtualqtype = $this->get_virtual_qtype();
895         return $virtualqtype->grade_responses($numericalquestion, $state, $cmoptions) ;
896     }
899     // ULPGC ecastro
900     function check_response(&$question, &$state) {
901         // Forward the checking to the virtual qtype
902         // We modify the question to look like a numerical question
903         $numericalquestion = clone($question);
904         $numericalquestion->options = clone($question->options);
905         foreach ($question->options->answers as $key => $answer) {
906             $numericalquestion->options->answers[$key] = clone($answer);
907         }
908         foreach ($numericalquestion->options->answers as $key => $answer) {
909             $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
910             $answer->answer = $this->substitute_variables_and_eval($answer->answer,
911                 $state->options->dataset);
912         }
913         $virtualqtype = $this->get_virtual_qtype();
914         return $virtualqtype->check_response($numericalquestion, $state) ;
915     }
917     // ULPGC ecastro
918     function get_actual_response(&$question, &$state) {
919         // Substitute variables in questiontext before giving the data to the
920         // virtual type
921         $virtualqtype = $this->get_virtual_qtype();
922         $unit = $virtualqtype->get_default_numerical_unit($question);
924         // We modify the question to look like a numerical question
925         $numericalquestion = clone($question);
926         $numericalquestion->options = clone($question->options);
927         foreach ($question->options->answers as $key => $answer) {
928             $numericalquestion->options->answers[$key] = clone($answer);
929         }
930         foreach ($numericalquestion->options->answers as $key => $answer) {
931             $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
932             $answer->answer = $this->substitute_variables_and_eval($answer->answer,
933                 $state->options->dataset);
934             // apply_unit
935         }
936         $numericalquestion->questiontext = $this->substitute_variables_and_eval(
937             $numericalquestion->questiontext, $state->options->dataset);
938         $responses = $virtualqtype->get_all_responses($numericalquestion, $state);
939         $response = reset($responses->responses);
940         $correct = $response->answer.' : ';
942         $responses = $virtualqtype->get_actual_response($numericalquestion, $state);
944         foreach ($responses as $key=>$response){
945             $responses[$key] = $correct.$response;
946         }
948         return $responses;
949     }
951     function create_virtual_qtype() {
952         global $CFG;
953         require_once("$CFG->dirroot/question/type/numerical/questiontype.php");
954         return new question_numerical_qtype();
955     }
957     function supports_dataset_item_generation() {
958         // Calcualted support generation of randomly distributed number data
959         return true;
960     }
961     function custom_generator_tools_part(&$mform, $idx, $j){
963         $minmaxgrp = array();
964         $minmaxgrp[] =& $mform->createElement('text', "calcmin[$idx]", get_string('calcmin', 'qtype_calculated'));
965         $minmaxgrp[] =& $mform->createElement('text', "calcmax[$idx]", get_string('calcmax', 'qtype_calculated'));
966         $mform->addGroup($minmaxgrp, 'minmaxgrp', get_string('minmax', 'qtype_calculated'), ' - ', false);
967         $mform->setType("calcmin[$idx]", PARAM_NUMBER);
968         $mform->setType("calcmax[$idx]", PARAM_NUMBER);
970         $precisionoptions = range(0, 10);
971         $mform->addElement('select', "calclength[$idx]", get_string('calclength', 'qtype_calculated'), $precisionoptions);
973         $distriboptions = array('uniform' => get_string('uniform', 'qtype_calculated'), 'loguniform' => get_string('loguniform', 'qtype_calculated'));
974         $mform->addElement('select', "calcdistribution[$idx]", get_string('calcdistribution', 'qtype_calculated'), $distriboptions);
975     }
977     function custom_generator_set_data($datasetdefs, $formdata){
978         $idx = 1;
979         foreach ($datasetdefs as $datasetdef){
980             if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~', $datasetdef->options, $regs)) {
981                 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
982                 $formdata["calcdistribution[$idx]"] = $regs[1];
983                 $formdata["calcmin[$idx]"] = $regs[2];
984                 $formdata["calcmax[$idx]"] = $regs[3];
985                 $formdata["calclength[$idx]"] = $regs[4];
986             }
987             $idx++;
988         }
989         return $formdata;
990     }
992     function custom_generator_tools($datasetdef) {
993         global $OUTPUT;
994         if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
995             $datasetdef->options, $regs)) {
996                 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
997                 for ($i = 0 ; $i<10 ; ++$i) {
998                     $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
999                         ? 'decimals'
1000                         : 'significantfigures'), 'quiz', $i);
1001                 }
1002                 $menu1 = html_writer::select($lengthoptions, 'calclength[]', $regs[4], null);
1004                 $options = array('uniform' => get_string('uniform', 'quiz'), 'loguniform' => get_string('loguniform', 'quiz'));
1005                 $menu2 = html_writer::select($options, 'calcdistribution[]', $regs[1], null);
1006                 return '<input type="submit" onclick="'
1007                     . "getElementById('addform').regenerateddefid.value='$defid'; return true;"
1008                     .'" value="'. get_string('generatevalue', 'quiz') . '"/><br/>'
1009                     . '<input type="text" size="3" name="calcmin[]" '
1010                     . " value=\"$regs[2]\"/> &amp; <input name=\"calcmax[]\" "
1011                     . ' type="text" size="3" value="' . $regs[3] .'"/> '
1012                     . $menu1 . '<br/>'
1013                     . $menu2;
1014             } else {
1015                 return '';
1016             }
1017     }
1020     function update_dataset_options($datasetdefs, $form) {
1021         global $OUTPUT;
1022         // Do we have informatin about new options???
1023         if (empty($form->definition) || empty($form->calcmin)
1024             || empty($form->calcmax) || empty($form->calclength)
1025             || empty($form->calcdistribution)) {
1026                 // I guess not
1028             } else {
1029                 // Looks like we just could have some new information here
1030                 $uniquedefs = array_values(array_unique($form->definition));
1031                 foreach ($uniquedefs as $key => $defid) {
1032                     if (isset($datasetdefs[$defid])
1033                         && is_numeric($form->calcmin[$key+1])
1034                         && is_numeric($form->calcmax[$key+1])
1035                         && is_numeric($form->calclength[$key+1])) {
1036                             switch     ($form->calcdistribution[$key+1]) {
1037                             case 'uniform': case 'loguniform':
1038                                 $datasetdefs[$defid]->options =
1039                                     $form->calcdistribution[$key+1] . ':'
1040                                     . $form->calcmin[$key+1] . ':'
1041                                     . $form->calcmax[$key+1] . ':'
1042                                     . $form->calclength[$key+1];
1043                                 break;
1044                             default:
1045                                 echo $OUTPUT->notification("Unexpected distribution ".$form->calcdistribution[$key+1]);
1046                             }
1047                         }
1048                 }
1049             }
1051         // Look for empty options, on which we set default values
1052         foreach ($datasetdefs as $defid => $def) {
1053             if (empty($def->options)) {
1054                 $datasetdefs[$defid]->options = 'uniform:1.0:10.0:1';
1055             }
1056         }
1057         return $datasetdefs;
1058     }
1060     function save_question_calculated($question, $fromform){
1061         global $DB;
1063         foreach ($question->options->answers as $key => $answer) {
1064             if ($options = $DB->get_record('question_calculated', array('answer' => $key))) {
1065                 $options->tolerance = trim($fromform->tolerance[$key]);
1066                 $options->tolerancetype  = trim($fromform->tolerancetype[$key]);
1067                 $options->correctanswerlength  = trim($fromform->correctanswerlength[$key]);
1068                 $options->correctanswerformat  = trim($fromform->correctanswerformat[$key]);
1069                 $DB->update_record('question_calculated', $options);
1070             }
1071         }
1072     }
1074     /**
1075      * This function get the dataset items using id as unique parameter and return an
1076      * array with itemnumber as index sorted ascendant
1077      * If the multiple records with the same itemnumber exist, only the newest one
1078      * i.e with the greatest id is used, the others are ignored but not deleted.
1079      * MDL-19210
1080      */
1081     function get_database_dataset_items($definition){
1082         global $CFG, $DB;
1083         $databasedataitems = $DB->get_records_sql( // Use number as key!!
1084             " SELECT id , itemnumber, definition,  value
1085             FROM {question_dataset_items}
1086             WHERE definition = $definition order by id DESC ", array($definition));
1087         $dataitems = Array();
1088         foreach($databasedataitems as $id => $dataitem  ){
1089             if (!isset($dataitems[$dataitem->itemnumber])){
1090                 $dataitems[$dataitem->itemnumber] = $dataitem ;
1091             }else {
1092                 // deleting the unused records could be added here
1093             }
1094         }
1095         ksort($dataitems);
1096         return $dataitems ;
1097     }
1099     function save_dataset_items($question, $fromform){
1100         global $CFG, $DB;
1101         // max datasets = 100 items
1102         $max100 = CALCULATEDQUESTIONMAXITEMNUMBER ;
1103         $synchronize = false ;
1104         if(isset($fromform->nextpageparam["forceregeneration"])) {
1105             $regenerate = $fromform->nextpageparam["forceregeneration"];
1106         }else{
1107             $regenerate = 0 ;
1108         }
1109         if (empty($question->options)) {
1110             $this->get_question_options($question);
1111         }
1112         if(!empty($question->options->synchronize)){
1113             $synchronize = true ;
1114         }
1117         //get the old datasets for this question
1118         $datasetdefs = $this->get_dataset_definitions($question->id, array());
1119         // Handle generator options...
1120         $olddatasetdefs = fullclone($datasetdefs);
1121         $datasetdefs = $this->update_dataset_options($datasetdefs, $fromform);
1122         $maxnumber = -1;
1123         foreach ($datasetdefs as $defid => $datasetdef) {
1124             if (isset($datasetdef->id)
1125                 && $datasetdef->options != $olddatasetdefs[$defid]->options) {
1126                     // Save the new value for options
1127                     $DB->update_record('question_dataset_definitions', $datasetdef);
1129                 }
1130             // Get maxnumber
1131             if ($maxnumber == -1 || $datasetdef->itemcount < $maxnumber) {
1132                 $maxnumber = $datasetdef->itemcount;
1133             }
1134         }
1135         // Handle adding and removing of dataset items
1136         $i = 1;
1137         if ($maxnumber > CALCULATEDQUESTIONMAXITEMNUMBER ){
1138             $maxnumber = CALCULATEDQUESTIONMAXITEMNUMBER ;
1139         }
1141         ksort($fromform->definition);
1142         foreach ($fromform->definition as $key => $defid) {
1143             //if the delete button has not been pressed then skip the datasetitems
1144             //in the 'add item' part of the form.
1145             if ( $i > count($datasetdefs)*$maxnumber ) {
1146                 break;
1147             }
1148             $addeditem = new stdClass();
1149             $addeditem->definition = $datasetdefs[$defid]->id;
1150             $addeditem->value = $fromform->number[$i];
1151             $addeditem->itemnumber = ceil($i / count($datasetdefs));
1153             if ($fromform->itemid[$i]) {
1154                 // Reuse any previously used record
1155                 $addeditem->id = $fromform->itemid[$i];
1156                 $DB->update_record('question_dataset_items', $addeditem);
1157             } else {
1158                 $DB->insert_record('question_dataset_items', $addeditem);
1159             }
1161             $i++;
1162         }
1163         if (isset($addeditem->itemnumber) && $maxnumber < $addeditem->itemnumber
1164             && $addeditem->itemnumber < CALCULATEDQUESTIONMAXITEMNUMBER ){
1165                 $maxnumber = $addeditem->itemnumber;
1166                 foreach ($datasetdefs as $key => $newdef) {
1167                     if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
1168                         $newdef->itemcount = $maxnumber;
1169                         // Save the new value for options
1170                         $DB->update_record('question_dataset_definitions', $newdef);
1171                     }
1172                 }
1173             }
1174         // adding supplementary items
1175         $numbertoadd =0;
1176         if (isset($fromform->addbutton) && $fromform->selectadd > 0 && $maxnumber < CALCULATEDQUESTIONMAXITEMNUMBER ) {
1177             $numbertoadd =$fromform->selectadd ;
1178             if ( $max100 - $maxnumber < $numbertoadd ) {
1179                 $numbertoadd = $max100 - $maxnumber ;
1180             }
1181             //add the other items.
1182             // Generate a new dataset item (or reuse an old one)
1183             foreach ($datasetdefs as $defid => $datasetdef) {
1184                 // in case that for category datasets some new items has been added
1185                 // get actual values
1186                 // fix regenerate for this datadefs
1187                 $defregenerate = 0 ;
1188                 if($synchronize && !empty ($fromform->nextpageparam["datasetregenerate[$datasetdef->name"])) {
1189                     $defregenerate = 1 ;
1190                 }else if(!$synchronize && (($regenerate == 1 && $datasetdef->category == 0) ||$regenerate == 2 )){
1191                     $defregenerate = 1 ;
1192                 }
1193                 if (isset($datasetdef->id)) {
1194                     $datasetdefs[$defid]->items = $this->get_database_dataset_items($datasetdef->id);
1195                 }
1196                 for ($numberadded =$maxnumber+1 ; $numberadded <= $maxnumber+$numbertoadd ; $numberadded++){
1197                     if (isset($datasetdefs[$defid]->items[$numberadded])  ){
1198                         // in case of regenerate it modifies the already existing record
1199                         if ( $defregenerate  ) {
1200                             $datasetitem = new stdClass;
1201                             $datasetitem->id = $datasetdefs[$defid]->items[$numberadded]->id;
1202                             $datasetitem->definition = $datasetdef->id ;
1203                             $datasetitem->itemnumber = $numberadded;
1204                             $datasetitem->value = $this->generate_dataset_item($datasetdef->options);
1205                             $DB->update_record('question_dataset_items', $datasetitem);
1206                         }
1207                         //if not regenerate do nothing as there is already a record
1208                     } else {
1209                         $datasetitem = new stdClass;
1210                         $datasetitem->definition = $datasetdef->id ;
1211                         $datasetitem->itemnumber = $numberadded;
1212                         if ($this->supports_dataset_item_generation()) {
1213                             $datasetitem->value = $this->generate_dataset_item($datasetdef->options);
1214                         } else {
1215                             $datasetitem->value = '';
1216                         }
1217                         $DB->insert_record('question_dataset_items', $datasetitem);
1218                     }
1219                 }//for number added
1220             }// datasetsdefs end
1221             $maxnumber += $numbertoadd ;
1222             foreach ($datasetdefs as $key => $newdef) {
1223                 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
1224                     $newdef->itemcount = $maxnumber;
1225                     // Save the new value for options
1226                     $DB->update_record('question_dataset_definitions', $newdef);
1227                 }
1228             }
1229         }
1231         if (isset($fromform->deletebutton))  {
1232             if(isset($fromform->selectdelete)) $newmaxnumber = $maxnumber-$fromform->selectdelete ;
1233             else $newmaxnumber = $maxnumber-1 ;
1234             if ($newmaxnumber < 0 ) $newmaxnumber = 0 ;
1235             foreach ($datasetdefs as $datasetdef) {
1236                 if ($datasetdef->itemcount == $maxnumber) {
1237                     $datasetdef->itemcount= $newmaxnumber ;
1238                     $DB->update_record('question_dataset_definitions', $datasetdef);
1239                 }
1240             }
1241         }
1242     }
1243     function generate_dataset_item($options) {
1244         if (!preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
1245             $options, $regs)) {
1246                 // Unknown options...
1247                 return false;
1248             }
1249         if ($regs[1] == 'uniform') {
1250             $nbr = $regs[2] + ($regs[3]-$regs[2])*mt_rand()/mt_getrandmax();
1251             return sprintf("%.".$regs[4]."f",$nbr);
1253         } else if ($regs[1] == 'loguniform') {
1254             $log0 = log(abs($regs[2])); // It would have worked the other way to
1255             $nbr = exp($log0 + (log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax());
1256             return sprintf("%.".$regs[4]."f",$nbr);
1258         } else {
1259             print_error('disterror', 'question', '', $regs[1]);
1260         }
1261         return '';
1262     }
1264     function comment_header($question) {
1265         //$this->get_question_options($question);
1266         $strheader = '';
1267         $delimiter = '';
1269         $answers = $question->options->answers;
1271         foreach ($answers as $key => $answer) {
1272             if (is_string($answer)) {
1273                 $strheader .= $delimiter.$answer;
1274             } else {
1275                 $strheader .= $delimiter.$answer->answer;
1276             }
1277             $delimiter = '<br/><br/><br/>';
1278         }
1279         return $strheader;
1280     }
1282     function comment_on_datasetitems($qtypeobj, $questionid, $questiontext, $answers, $data, $number) {
1283         global $DB, $QTYPES;
1284         $comment = new stdClass;
1285         $comment->stranswers = array();
1286         $comment->outsidelimit = false ;
1287         $comment->answers = array();
1288         /// Find a default unit:
1289         if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units', array('question'=> $questionid, 'multiplier' => 1.0))) {
1290             $unit = $unit->unit;
1291         } else {
1292             $unit = '';
1293         }
1295         $answers = fullclone($answers);
1296         $strmin = get_string('min', 'quiz');
1297         $strmax = get_string('max', 'quiz');
1298         $errors = '';
1299         $delimiter = ': ';
1300         $virtualqtype =  $qtypeobj->get_virtual_qtype();//& $QTYPES['numerical'];
1301         foreach ($answers as $key => $answer) {
1302             $formula = $this->substitute_variables($answer->answer,$data);
1303             $formattedanswer = qtype_calculated_calculate_answer(
1304                 $answer->answer, $data, $answer->tolerance,
1305                 $answer->tolerancetype, $answer->correctanswerlength,
1306                 $answer->correctanswerformat, $unit);
1307             if ( $formula === '*'){
1308                 $answer->min = ' ';
1309                 $formattedanswer->answer = $answer->answer ;
1310             }else {
1311                 eval('$answer->answer = '.$formula.';') ;
1312                 $virtualqtype->get_tolerance_interval($answer);
1313             }
1314             if ($answer->min === '') {
1315                 // This should mean that something is wrong
1316                 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
1317             } else if ($formula === '*'){
1318                 $comment->stranswers[$key] = $formula.' = '.get_string('anyvalue','qtype_calculated').'<br/><br/><br/>';
1319             }else{
1320                 $comment->stranswers[$key]= $formula.' = '.$formattedanswer->answer.'<br/>' ;
1321                 $correcttrue->correct = $formattedanswer->answer ;
1322                 $correcttrue->true = $answer->answer ;
1323                 if ($formattedanswer->answer < $answer->min || $formattedanswer->answer > $answer->max){
1324                     $comment->outsidelimit = true ;
1325                     $comment->answers[$key] = $key;
1326                     $comment->stranswers[$key] .=get_string('trueansweroutsidelimits','qtype_calculated',$correcttrue);//<span class="error">ERROR True answer '..' outside limits</span>';
1327                 }else {
1328                     $comment->stranswers[$key] .=get_string('trueanswerinsidelimits','qtype_calculated',$correcttrue);//' True answer :'.$calculated->trueanswer.' inside limits';
1329                 }
1330                 $comment->stranswers[$key] .='<br/>';
1331                 $comment->stranswers[$key] .= $strmin.$delimiter.$answer->min.' --- ';
1332                 $comment->stranswers[$key] .= $strmax.$delimiter.$answer->max;
1333                 $comment->stranswers[$key] .='';
1334             }
1335         }
1336         return fullclone($comment);
1337     }
1338     function multichoice_comment_on_datasetitems($questionid, $questiontext, $answers,$data, $number) {
1339         global $DB;
1340         $comment = new stdClass;
1341         $comment->stranswers = array();
1342         $comment->outsidelimit = false ;
1343         $comment->answers = array();
1344         /// Find a default unit:
1345         if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units', array('question'=> $questionid, 'multiplier' => 1.0))) {
1346             $unit = $unit->unit;
1347         } else {
1348             $unit = '';
1349         }
1351         $answers = fullclone($answers);
1352         $strmin = get_string('min', 'quiz');
1353         $strmax = get_string('max', 'quiz');
1354         $errors = '';
1355         $delimiter = ': ';
1356         foreach ($answers as $key => $answer) {
1357             $answer->answer = $this->substitute_variables($answer->answer, $data);
1358             //evaluate the equations i.e {=5+4)
1359             $qtext = "";
1360             $qtextremaining = $answer->answer ;
1361             while  (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
1362                 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
1363                 $qtext =$qtext.$qtextsplits[0];
1364                 $qtextremaining = $qtextsplits[1];
1365                 if (empty($regs1[1])) {
1366                     $str = '';
1367                 } else {
1368                     if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
1369                         $str=$formulaerrors ;
1370                     }else {
1371                         eval('$str = '.$regs1[1].';');
1373                         $texteval= qtype_calculated_calculate_answer(
1374                             $str, $data, $answer->tolerance,
1375                             $answer->tolerancetype, $answer->correctanswerlength,
1376                             $answer->correctanswerformat, '');
1377                         $str = $texteval->answer;
1379                     }
1380                 }
1381                 $qtext = $qtext.$str ;
1382             }
1383             $answer->answer = $qtext.$qtextremaining ; ;
1384             $comment->stranswers[$key]= $answer->answer ;
1387           /*  $formula = $this->substitute_variables($answer->answer,$data);
1388             $formattedanswer = qtype_calculated_calculate_answer(
1389                     $answer->answer, $data, $answer->tolerance,
1390                     $answer->tolerancetype, $answer->correctanswerlength,
1391                     $answer->correctanswerformat, $unit);
1392                     if ( $formula === '*'){
1393                         $answer->min = ' ';
1394                         $formattedanswer->answer = $answer->answer ;
1395                     }else {
1396                         eval('$answer->answer = '.$formula.';') ;
1397                         $virtualqtype->get_tolerance_interval($answer);
1398                     }
1399             if ($answer->min === '') {
1400                 // This should mean that something is wrong
1401                 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
1402             } else if ($formula === '*'){
1403                 $comment->stranswers[$key] = $formula.' = '.get_string('anyvalue','qtype_calculated').'<br/><br/><br/>';
1404             }else{
1405                 $comment->stranswers[$key]= $formula.' = '.$formattedanswer->answer.'<br/>' ;
1406                 $comment->stranswers[$key] .= $strmin. $delimiter.$answer->min.'---';
1407                 $comment->stranswers[$key] .= $strmax.$delimiter.$answer->max;
1408                 $comment->stranswers[$key] .='<br/>';
1409                 $correcttrue->correct = $formattedanswer->answer ;
1410                 $correcttrue->true = $answer->answer ;
1411                 if ($formattedanswer->answer < $answer->min || $formattedanswer->answer > $answer->max){
1412                     $comment->outsidelimit = true ;
1413                     $comment->answers[$key] = $key;
1414                     $comment->stranswers[$key] .=get_string('trueansweroutsidelimits','qtype_calculated',$correcttrue);//<span class="error">ERROR True answer '..' outside limits</span>';
1415                 }else {
1416                     $comment->stranswers[$key] .=get_string('trueanswerinsidelimits','qtype_calculated',$correcttrue);//' True answer :'.$calculated->trueanswer.' inside limits';
1417                 }
1418                 $comment->stranswers[$key] .='';
1419           }*/
1420         }
1421         return fullclone($comment);
1422     }
1424     function tolerance_types() {
1425         return array('1'  => get_string('relative', 'quiz'),
1426             '2'  => get_string('nominal', 'quiz'),
1427             '3'  => get_string('geometric', 'quiz'));
1428     }
1430     function dataset_options($form, $name, $mandatory=true,$renameabledatasets=false) {
1431         // Takes datasets from the parent implementation but
1432         // filters options that are currently not accepted by calculated
1433         // It also determines a default selection...
1434         //$renameabledatasets not implemented anmywhere
1435         list($options, $selected) = $this->dataset_options_from_database($form, $name,'','qtype_calculated');
1436         //  list($options, $selected) = $this->dataset_optionsa($form, $name);
1438         foreach ($options as $key => $whatever) {
1439             if (!preg_match('~^1-~', $key) && $key != '0') {
1440                 unset($options[$key]);
1441             }
1442         }
1443         if (!$selected) {
1444             if ($mandatory){
1445                 $selected =  "1-0-$name"; // Default
1446             }else {
1447                 $selected = "0"; // Default
1448             }
1449         }
1450         return array($options, $selected);
1451     }
1453     function construct_dataset_menus($form, $mandatorydatasets,
1454         $optionaldatasets) {
1455             global $OUTPUT;
1456             $datasetmenus = array();
1457             foreach ($mandatorydatasets as $datasetname) {
1458                 if (!isset($datasetmenus[$datasetname])) {
1459                     list($options, $selected) =
1460                         $this->dataset_options($form, $datasetname);
1461                     unset($options['0']); // Mandatory...
1462                     $datasetmenus[$datasetname] = html_writer::select($options, 'dataset[]', $selected, null);
1463                 }
1464             }
1465             foreach ($optionaldatasets as $datasetname) {
1466                 if (!isset($datasetmenus[$datasetname])) {
1467                     list($options, $selected) =
1468                         $this->dataset_options($form, $datasetname);
1469                     $datasetmenus[$datasetname] = html_writer::select($options, 'dataset[]', $selected, null);
1470                 }
1471             }
1472             return $datasetmenus;
1473         }
1475     function print_question_grading_details(&$question, &$state, &$cmoptions, &$options) {
1476         $virtualqtype = $this->get_virtual_qtype();
1477         $virtualqtype->print_question_grading_details($question, $state, $cmoptions, $options) ;
1478     }
1480     function get_correct_responses(&$question, &$state) {
1481         // virtual type for printing
1482         $virtualqtype = $this->get_virtual_qtype();
1483         $unit = $this->get_default_numerical_unit($question,$virtualqtype);
1484         // We modify the question to look like a numerical question
1485         $this->convert_answers($question, $state);
1486         return $virtualqtype->get_correct_responses($question, $state) ;
1487     }
1489     function substitute_variables($str, $dataset) {
1490         global $OUTPUT ;
1491         //  testing for wrong numerical values
1492         // all calculations used this function so testing here should be OK
1494         foreach ($dataset as $name => $value) {
1495             $val = $value ;
1496             if(! is_numeric($val)){
1497                 $a = new stdClass;
1498                 $a->name = '{'.$name.'}' ;
1499                 $a->value = $value ;
1500                 echo $OUTPUT->notification(get_string('notvalidnumber','qtype_calculated',$a));
1501                 $val = 1.0 ;
1502             }
1503             if($val < 0 ){
1504                 $str = str_replace('{'.$name.'}', '('.$val.')', $str);
1505             } else {
1506                 $str = str_replace('{'.$name.'}', $val, $str);
1507             }
1508         }
1509         return $str;
1510     }
1511     function evaluate_equations($str, $dataset){
1512         $formula = $this->substitute_variables($str, $dataset) ;
1513         if ($error = qtype_calculated_find_formula_errors($formula)) {
1514             return $error;
1515         }
1516         return $str;
1517     }
1520     function substitute_variables_and_eval($str, $dataset) {
1521         $formula = $this->substitute_variables($str, $dataset) ;
1522         if ($error = qtype_calculated_find_formula_errors($formula)) {
1523             return $error;
1524         }
1525         /// Calculate the correct answer
1526         if (empty($formula)) {
1527             $str = '';
1528         } else if ($formula === '*'){
1529             $str = '*';
1530         } else {
1531             eval('$str = '.$formula.';');
1532         }
1533         return $str;
1534     }
1536     function get_dataset_definitions($questionid, $newdatasets) {
1537         global $DB;
1538         //get the existing datasets for this question
1539         $datasetdefs = array();
1540         if (!empty($questionid)) {
1541             global $CFG;
1542             $sql = "SELECT i.*
1543                       FROM {question_datasets} d, {question_dataset_definitions} i
1544                      WHERE d.question = ? AND d.datasetdefinition = i.id";
1545             if ($records = $DB->get_records_sql($sql, array($questionid))) {
1546                 foreach ($records as $r) {
1547                     $datasetdefs["$r->type-$r->category-$r->name"] = $r;
1548                 }
1549             }
1550         }
1552         foreach ($newdatasets as $dataset) {
1553             if (!$dataset) {
1554                 continue; // The no dataset case...
1555             }
1557             if (!isset($datasetdefs[$dataset])) {
1558                 //make new datasetdef
1559                 list($type, $category, $name) = explode('-', $dataset, 3);
1560                 $datasetdef = new stdClass;
1561                 $datasetdef->type = $type;
1562                 $datasetdef->name = $name;
1563                 $datasetdef->category  = $category;
1564                 $datasetdef->itemcount = 0;
1565                 $datasetdef->options   = 'uniform:1.0:10.0:1';
1566                 $datasetdefs[$dataset] = clone($datasetdef);
1567             }
1568         }
1569         return $datasetdefs;
1570     }
1572     function save_dataset_definitions($form) {
1573         global $DB;
1574         // save synchronize
1576         if (empty($form->dataset)) {
1577             $form->dataset = array();
1578         }
1579         // Save datasets
1580         $datasetdefinitions = $this->get_dataset_definitions($form->id, $form->dataset);
1581         $tmpdatasets = array_flip($form->dataset);
1582         $defids = array_keys($datasetdefinitions);
1583         foreach ($defids as $defid) {
1584             $datasetdef = &$datasetdefinitions[$defid];
1585             if (isset($datasetdef->id)) {
1586                 if (!isset($tmpdatasets[$defid])) {
1587                     // This dataset is not used any more, delete it
1588                     $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $datasetdef->id));
1589                     if ($datasetdef->category == 0) { // Question local dataset
1590                         $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id));
1591                         $DB->delete_records('question_dataset_items', array('definition' => $datasetdef->id));
1592                     }
1593                 }
1594                 // This has already been saved or just got deleted
1595                 unset($datasetdefinitions[$defid]);
1596                 continue;
1597             }
1599             $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
1601             if (0 != $datasetdef->category) {
1602                 // We need to look for already existing
1603                 // datasets in the category.
1604                 // By first creating the datasetdefinition above we
1605                 // can manage to automatically take care of
1606                 // some possible realtime concurrence
1607                 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
1608                          "type = ?
1609                       AND name = ?
1610                       AND category = ?
1611                       AND id < ?
1612                     ORDER BY id DESC", array($datasetdef->type, $datasetdef->name, $datasetdef->category, $datasetdef->id))) {
1614                     while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1615                         $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id));
1616                         $datasetdef = $olderdatasetdef;
1617                     }
1618                 }
1619             }
1621             // Create relation to this dataset:
1622             $questiondataset = new stdClass;
1623             $questiondataset->question = $form->id;
1624             $questiondataset->datasetdefinition = $datasetdef->id;
1625             $DB->insert_record('question_datasets', $questiondataset);
1626             unset($datasetdefinitions[$defid]);
1627         }
1629         // Remove local obsolete datasets as well as relations
1630         // to datasets in other categories:
1631         if (!empty($datasetdefinitions)) {
1632             foreach ($datasetdefinitions as $def) {
1633                 $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $def->id));
1635                 if ($def->category == 0) { // Question local dataset
1636                     $DB->delete_records('question_dataset_definitions', array('id' => $def->id));
1637                     $DB->delete_records('question_dataset_items', array('definition' => $def->id));
1638                 }
1639             }
1640         }
1641     }
1642     /** This function create a copy of the datasets ( definition and dataitems)
1643      * from the preceding question if they remain in the new question
1644      * otherwise its create the datasets that have been added as in the
1645      * save_dataset_definitions()
1646      */
1647     function save_as_new_dataset_definitions($form, $initialid) {
1648         global $CFG, $DB;
1649         // Get the datasets from the intial question
1650         $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset);
1651         // $tmpdatasets contains those of the new question
1652         $tmpdatasets = array_flip($form->dataset);
1653         $defids = array_keys($datasetdefinitions);// new datasets
1654         foreach ($defids as $defid) {
1655             $datasetdef = &$datasetdefinitions[$defid];
1656             if (isset($datasetdef->id)) {
1657                 // This dataset exist in the initial question
1658                 if (!isset($tmpdatasets[$defid])) {
1659                     // do not exist in the new question so ignore
1660                     unset($datasetdefinitions[$defid]);
1661                     continue;
1662                 }
1663                 // create a copy but not for category one
1664                 if (0 == $datasetdef->category) {
1665                     $olddatasetid = $datasetdef->id ;
1666                     $olditemcount = $datasetdef->itemcount ;
1667                     $datasetdef->itemcount =0;
1668                     $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
1669                     //copy the dataitems
1670                     $olditems = $this->get_database_dataset_items($olddatasetid);
1671                     if (count($olditems) > 0 ) {
1672                         $itemcount = 0;
1673                         foreach($olditems as $item ){
1674                             $item->definition = $datasetdef->id;
1675                             $DB->insert_record('question_dataset_items', $item);
1676                             $itemcount++;
1677                         }
1678                         //update item count to olditemcount if
1679                         // at least this number of items has been recover from the database
1680                         if( $olditemcount <= $itemcount ) {
1681                             $datasetdef->itemcount = $olditemcount;
1682                         } else {
1683                             $datasetdef->itemcount = $itemcount ;
1684                         }
1685                         $DB->update_record('question_dataset_definitions', $datasetdef);
1686                     } // end of  copy the dataitems
1687                 }// end of  copy the datasetdef
1688                 // Create relation to the new question with this
1689                 // copy as new datasetdef from the initial question
1690                 $questiondataset = new stdClass;
1691                 $questiondataset->question = $form->id;
1692                 $questiondataset->datasetdefinition = $datasetdef->id;
1693                 $DB->insert_record('question_datasets', $questiondataset);
1694                 unset($datasetdefinitions[$defid]);
1695                 continue;
1696             }// end of datasetdefs from the initial question
1697             // really new one code similar to save_dataset_definitions()
1698             $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
1700             if (0 != $datasetdef->category) {
1701                 // We need to look for already existing
1702                 // datasets in the category.
1703                 // By first creating the datasetdefinition above we
1704                 // can manage to automatically take care of
1705                 // some possible realtime concurrence
1706                 if ($olderdatasetdefs = $DB->get_records_select(
1707                     'question_dataset_definitions',
1708                     "type = ?
1709                     AND name = ?
1710                     AND category = ?
1711                     AND id < ?
1712                     ORDER BY id DESC", array($datasetdef->type, $datasetdef->name, $datasetdef->category, $datasetdef->id))) {
1714                         while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1715                             $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id));
1716                             $datasetdef = $olderdatasetdef;
1717                         }
1718                     }
1719             }
1721             // Create relation to this dataset:
1722             $questiondataset = new stdClass;
1723             $questiondataset->question = $form->id;
1724             $questiondataset->datasetdefinition = $datasetdef->id;
1725             $DB->insert_record('question_datasets', $questiondataset);
1726             unset($datasetdefinitions[$defid]);
1727         }
1729         // Remove local obsolete datasets as well as relations
1730         // to datasets in other categories:
1731         if (!empty($datasetdefinitions)) {
1732             foreach ($datasetdefinitions as $def) {
1733                 $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $def->id));
1735                 if ($def->category == 0) { // Question local dataset
1736                     $DB->delete_records('question_dataset_definitions', array('id' => $def->id));
1737                     $DB->delete_records('question_dataset_items', array('definition' => $def->id));
1738                 }
1739             }
1740         }
1741     }
1743     /// Dataset functionality
1744     function pick_question_dataset($question, $datasetitem) {
1745         // Select a dataset in the following format:
1746         // An array indexed by the variable names (d.name) pointing to the value
1747         // to be substituted
1748         global $CFG, $DB;
1749         if (!$dataitems = $DB->get_records_sql(
1750             "SELECT i.id, d.name, i.value
1751                FROM {question_dataset_definitions} d, {question_dataset_items} i, {question_datasets} q
1752               WHERE q.question = ? AND q.datasetdefinition = d.id AND d.id = i.definition AND i.itemnumber = ?
1753            ORDER by i.id DESC ", array($question->id, $datasetitem))) {
1754             print_error('cannotgetdsfordependent', 'question', '', array($question->id, $datasetitem));
1755         }
1756         $dataset = Array();
1757         foreach($dataitems as $id => $dataitem  ){
1758             if (!isset($dataset[$dataitem->name])){
1759                 $dataset[$dataitem->name] = $dataitem->value ;
1760             }else {
1761                 // deleting the unused records could be added here
1762             }
1763         }
1764         return $dataset;
1765     }
1767     function dataset_options_from_database($form, $name,$prefix='',$langfile='quiz') {
1768         global $CFG, $DB;
1769         $type = 1 ; // only type = 1 (i.e. old 'LITERAL') has ever been used
1771         // First options - it is not a dataset...
1772         $options['0'] = get_string($prefix.'nodataset', $langfile);
1773         // new question no local
1774         if (!isset($form->id) || $form->id == 0 ){
1775             $key = "$type-0-$name";
1776             $options[$key] = get_string($prefix."newlocal$type", $langfile);
1777             $currentdatasetdef = new stdClass;
1778             $currentdatasetdef->type = '0';
1779         }else {
1781             // Construct question local options
1782             $sql = "SELECT a.*
1783                 FROM {question_dataset_definitions} a, {question_datasets} b
1784                WHERE a.id = b.datasetdefinition AND a.type = '1' AND b.question = ? AND a.name = ?";
1785             $currentdatasetdef = $DB->get_record_sql($sql, array($form->id, $name));
1786             if (!$currentdatasetdef) {
1787                 $currentdatasetdef->type = '0';
1788             }
1789             $key = "$type-0-$name";
1790             if ($currentdatasetdef->type == $type
1791                 and $currentdatasetdef->category == 0) {
1792                     $options[$key] = get_string($prefix."keptlocal$type", $langfile);
1793                 } else {
1794                     $options[$key] = get_string($prefix."newlocal$type", $langfile);
1795                 }
1796         }
1797         // Construct question category options
1798         $categorydatasetdefs = $DB->get_records_sql(
1799             "SELECT b.question, a.*
1800             FROM {question_datasets} b,
1801             {question_dataset_definitions} a
1802             WHERE a.id = b.datasetdefinition
1803             AND a.type = '1'
1804             AND a.category = ?
1805             AND a.name = ?", array($form->category, $name));
1806         $type = 1 ;
1807         $key = "$type-$form->category-$name";
1808         if (!empty($categorydatasetdefs)){ // there is at least one with the same name
1809             if (isset($form->id) && isset($categorydatasetdefs[$form->id])) {// it is already used by this question
1810                 $options[$key] = get_string($prefix."keptcategory$type", $langfile);
1811             } else {
1812                 $options[$key] = get_string($prefix."existingcategory$type", $langfile);
1813             }
1814         } else {
1815             $options[$key] = get_string($prefix."newcategory$type", $langfile);
1816         }
1817         // All done!
1818         return array($options, $currentdatasetdef->type
1819             ? "$currentdatasetdef->type-$currentdatasetdef->category-$name"
1820             : '');
1821     }
1823     function find_dataset_names($text) {
1824         /// Returns the possible dataset names found in the text as an array
1825         /// The array has the dataset name for both key and value
1826         $datasetnames = array();
1827         while (preg_match('~\\{([[:alpha:]][^>} <{"\']*)\\}~', $text, $regs)) {
1828             $datasetnames[$regs[1]] = $regs[1];
1829             $text = str_replace($regs[0], '', $text);
1830         }
1831         return $datasetnames;
1832     }
1834     /**
1835      * This function retrieve the item count of the available category shareable
1836      * wild cards that is added as a comment displayed when a wild card with
1837      * the same name is displayed in datasetdefinitions_form.php
1838      */
1839     function get_dataset_definitions_category($form) {
1840         global $CFG, $DB;
1841         $datasetdefs = array();
1842         $lnamemax = 30;
1843         if (!empty($form->category)) {
1844             $sql = "SELECT i.*,d.*
1845                       FROM {question_datasets} d, {question_dataset_definitions} i
1846                      WHERE i.id = d.datasetdefinition AND i.category = ?";
1847             if ($records = $DB->get_records_sql($sql, array($form->category))) {
1848                 foreach ($records as $r) {
1849                     if ( !isset ($datasetdefs["$r->name"])) $datasetdefs["$r->name"] = $r->itemcount;
1850                 }
1851             }
1852         }
1853         return  $datasetdefs ;
1854     }
1856     /**
1857      * This function build a table showing the available category shareable
1858      * wild cards, their name, their definition (Min, Max, Decimal) , the item count
1859      * and the name of the question where they are used.
1860      * This table is intended to be add before the question text to help the user use
1861      * these wild cards
1862      */
1863     function print_dataset_definitions_category($form) {
1864         global $CFG, $DB;
1865         $datasetdefs = array();
1866         $lnamemax = 22;
1867         $namestr =get_string('name', 'quiz');
1868         $minstr=get_string('min', 'quiz');
1869         $maxstr=get_string('max', 'quiz');
1870         $rangeofvaluestr=get_string('minmax','qtype_calculated');
1871         $questionusingstr = get_string('usedinquestion','qtype_calculated');
1872         $itemscountstr = get_string('itemscount','qtype_calculated');
1873         $text ='';
1874         if (!empty($form->category)) {
1875             list($category) = explode(',', $form->category);
1876             $sql = "SELECT i.*,d.*
1877                 FROM {question_datasets} d,
1878         {question_dataset_definitions} i
1879         WHERE i.id = d.datasetdefinition
1880         AND i.category = ?";
1881             if ($records = $DB->get_records_sql($sql, array($category))) {
1882                 foreach ($records as $r) {
1883                     $sql1 = "SELECT q.*
1884                                FROM {question} q
1885                               WHERE q.id = ?";
1886                     if ( !isset ($datasetdefs["$r->type-$r->category-$r->name"])){
1887                         $datasetdefs["$r->type-$r->category-$r->name"]= $r;
1888                     }
1889                     if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
1890                         $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question]->name =$questionb[$r->question]->name ;
1891                     }
1892                 }
1893             }
1894         }
1895         if (!empty ($datasetdefs)){
1897             $text ="<table width=\"100%\" border=\"1\"><tr><th  style=\"white-space:nowrap;\" class=\"header\" scope=\"col\" >$namestr</th><th   style=\"white-space:nowrap;\" class=\"header\" scope=\"col\">$rangeofvaluestr</th><th  style=\"white-space:nowrap;\" class=\"header\" scope=\"col\">$itemscountstr</th><th style=\"white-space:nowrap;\" class=\"header\" scope=\"col\">$questionusingstr</th></tr>";
1898             foreach ($datasetdefs as $datasetdef){
1899                 list($distribution, $min, $max,$dec) = explode(':', $datasetdef->options, 4);
1900                 $text .="<tr><td valign=\"top\" align=\"center\"> $datasetdef->name </td><td align=\"center\" valign=\"top\"> $min <strong>-</strong> $max </td><td align=\"right\" valign=\"top\">$datasetdef->itemcount&nbsp;&nbsp;</td><td align=\"left\">";
1901                 foreach ($datasetdef->questions as $qu) {
1902                     //limit the name length displayed
1903                     if (!empty($qu->name)) {
1904                         $qu->name = (strlen($qu->name) > $lnamemax) ?
1905                             substr($qu->name, 0, $lnamemax).'...' : $qu->name;
1906                     } else {
1907                         $qu->name = '';
1908                     }
1909                     $text .=" &nbsp;&nbsp; $qu->name <br/>";
1910                 }
1911                 $text .="</td></tr>";
1912             }
1913             $text .="</table>";
1914         }else{
1915             $text .=get_string('nosharedwildcard', 'qtype_calculated');
1916         }
1917         return  $text ;
1918     }
1920     /**
1921      * This function build a table showing the available category shareable
1922      * wild cards, their name, their definition (Min, Max, Decimal) , the item count
1923      * and the name of the question where they are used.
1924      * This table is intended to be add before the question text to help the user use
1925      * these wild cards
1926      */
1928     function print_dataset_definitions_category_shared($question,$datasetdefsq) {
1929         global $CFG, $DB;
1930         $datasetdefs = array();
1931         $lnamemax = 22;
1932         $namestr =get_string('name', 'quiz');
1933         $minstr=get_string('min', 'quiz');
1934         $maxstr=get_string('max', 'quiz');
1935         $rangeofvaluestr=get_string('minmax','qtype_calculated');
1936         $questionusingstr = get_string('usedinquestion','qtype_calculated');
1937         $itemscountstr = get_string('itemscount','qtype_calculated');
1938         $text ='';
1939         if (!empty($question->category)) {
1940             list($category) = explode(',', $question->category);
1941             $sql = "SELECT i.*,d.*
1942                       FROM {question_datasets} d, {question_dataset_definitions} i
1943                      WHERE i.id = d.datasetdefinition AND i.category = ?";
1944             if ($records = $DB->get_records_sql($sql, array($category))) {
1945                 foreach ($records as $r) {
1946                     $sql1 = "SELECT q.*
1947                                FROM {question} q
1948                               WHERE q.id = ?";
1949                     if ( !isset ($datasetdefs["$r->type-$r->category-$r->name"])){
1950                         $datasetdefs["$r->type-$r->category-$r->name"]= $r;
1951                     }
1952                     if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
1953                         $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question]->name =$questionb[$r->question]->name ;
1954                         $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question]->id =$questionb[$r->question]->id ;
1955                     }
1956                 }
1957             }
1958         }
1959         if (!empty ($datasetdefs)){
1961             $text ="<table width=\"100%\" border=\"1\"><tr><th  style=\"white-space:nowrap;\" class=\"header\" scope=\"col\" >$namestr</th>";
1962             $text .="<th  style=\"white-space:nowrap;\" class=\"header\" scope=\"col\">$itemscountstr</th>";
1963             $text .="<th style=\"white-space:nowrap;\" class=\"header\" scope=\"col\">&nbsp;&nbsp;$questionusingstr &nbsp;&nbsp; </th>";
1964             $text .="<th style=\"white-space:nowrap;\" class=\"header\" scope=\"col\">Quiz</th>";
1965             $text .="<th style=\"white-space:nowrap;\" class=\"header\" scope=\"col\">Attempts</th></tr>";
1966             foreach ($datasetdefs as $datasetdef){
1967                 list($distribution, $min, $max,$dec) = explode(':', $datasetdef->options, 4);
1968                 $count = count($datasetdef->questions);
1969                 $text .="<tr><td style=\"white-space:nowrap;\" valign=\"top\" align=\"center\" rowspan=\"$count\"> $datasetdef->name </td><td align=\"right\" valign=\"top\" rowspan=\"$count\" >$datasetdef->itemcount&nbsp;&nbsp;</td>";
1970                 //$text .="<td align=\"left\">";
1971                 $line = 0 ;
1972                 foreach ($datasetdef->questions as $qu) {
1973                     //limit the name length displayed
1974                     if (!empty($qu->name)) {
1975                         $qu->name = (strlen($qu->name) > $lnamemax) ?
1976                             substr($qu->name, 0, $lnamemax).'...' : $qu->name;
1977                     } else {
1978                         $qu->name = '';
1979                     }
1980                     if( $line ) {
1981                         $text .="<tr>";
1982                     }
1983                     $line++;
1984                     $text .="<td align=\"left\" style=\"white-space:nowrap;\" >$qu->name</td>";
1985                     $nb_of_quiz = 0;
1986                     $nb_of_attempts=0;
1987                     $used_in_quiz = false ;
1988                     if ($list = $DB->get_records('quiz_question_instances', array( 'question'=> $qu->id))){
1989                         $used_in_quiz = true;
1990                         foreach($list as $key => $li){
1991                             $nb_of_quiz ++;
1992                             if($att = $DB->get_records('quiz_attempts',array( 'quiz'=> $li->quiz, 'preview'=> '0'))){
1993                                 $nb_of_attempts+= count($att);
1994                             }
1995                         }
1996                     }
1997                     if($used_in_quiz){
1998                         $text .="<td align=\"center\">$nb_of_quiz</td>";
1999                     }else {
2000                         $text .="<td align=\"center\">0</td>";
2001                     }
2002                     if($used_in_quiz){
2003                         $text .="<td align=\"center\">$nb_of_attempts";
2004                     }else {
2005                         $text .="<td align=\"left\"><br/>";
2006                     }
2008                     $text .="</td></tr>";
2009                 }
2010             }
2011             $text .="</table>";
2012         }else{
2013             $text .=get_string('nosharedwildcard', 'qtype_calculated');
2014         }
2015         return  $text ;
2016     }
2018     function find_math_equations($text) {
2019         /// Returns the possible dataset names found in the text as an array
2020         /// The array has the dataset name for both key and value
2021         $equations = array();
2022  /*               $qtext = "";
2023         $qtextremaining = $numericalquestion->questiontext ;
2024         while  (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
2025       //  while  (preg_match('~\{=|%=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
2026             $qtextsplits = explode($regs1[0], $qtextremaining, 2);
2027             $qtext =$qtext.$qtextsplits[0];
2028             $qtextremaining = $qtextsplits[1];
2029             if (empty($regs1[1])) {
2030                     $str = '';
2031                 } else {
2032   */
2033         while (preg_match('~\{=([^[:space:]}]*)}~', $text, $regs)) {
2034             $equations[] = $regs[1];
2035             $text = str_replace($regs[0], '', $text);
2036         }
2037         return $equations;
2038     }
2040     function get_virtual_qtype() {
2041         global $QTYPES;
2042         $this->virtualqtype =& $QTYPES['numerical'];
2043         return $this->virtualqtype;
2044     }
2047     /// BACKUP FUNCTIONS ////////////////////////////
2049     /*
2050      * Backup the data in the question
2051      *
2052      * This is used in question/backuplib.php
2053      */
2054     function backup($bf,$preferences,$question,$level=6) {
2055         global $DB;
2056         $status = true;
2058         $calculateds = $DB->get_records("question_calculated",array("question" =>$question,"id"));
2059         //If there are calculated-s
2060         if ($calculateds) {
2061             //Iterate over each calculateds
2062             foreach ($calculateds as $calculated) {
2063                 $status = $status &&fwrite ($bf,start_tag("CALCULATED",$level,true));
2064                 //Print calculated contents
2065                 fwrite ($bf,full_tag("ANSWER",$level+1,false,$calculated->answer));
2066                 fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$calculated->tolerance));
2067                 fwrite ($bf,full_tag("TOLERANCETYPE",$level+1,false,$calculated->tolerancetype));
2068                 fwrite ($bf,full_tag("CORRECTANSWERLENGTH",$level+1,false,$calculated->correctanswerlength));
2069                 fwrite ($bf,full_tag("CORRECTANSWERFORMAT",$level+1,false,$calculated->correctanswerformat));
2070                 //Now backup numerical_units
2071                 $status = question_backup_numerical_units($bf,$preferences,$question,7);
2072                 //Now backup required dataset definitions and items...
2073                 $status = question_backup_datasets($bf,$preferences,$question,7);
2074                 //End calculated data
2075                 $status = $status &&fwrite ($bf,end_tag("CALCULATED",$level,true));
2076             }
2077             $calculated_options = $DB->get_records("question_calculated_options",array("questionid" => $question),"id");
2078             if ($calculated_options) {
2079                 //Iterate over each calculated_option
2080                 foreach ($calculated_options as $calculated_option) {
2081                     $status = fwrite ($bf,start_tag("CALCULATED_OPTIONS",$level,true));
2082                     //Print calculated_option contents
2083                     fwrite ($bf,full_tag("SYNCHRONIZE",$level+1,false,$calculated_option->synchronize));
2084                     fwrite ($bf,full_tag("SINGLE",$level+1,false,$calculated_option->single));
2085                     fwrite ($bf,full_tag("SHUFFLEANSWERS",$level+1,false,$calculated_option->shuffleanswers));
2086                     fwrite ($bf,full_tag("CORRECTFEEDBACK",$level+1,false,$calculated_option->correctfeedback));
2087                     fwrite ($bf,full_tag("PARTIALLYCORRECTFEEDBACK",$level+1,false,$calculated_option->partiallycorrectfeedback));
2088                     fwrite ($bf,full_tag("INCORRECTFEEDBACK",$level+1,false,$calculated_option->incorrectfeedback));
2089                     fwrite ($bf,full_tag("ANSWERNUMBERING",$level+1,false,$calculated_option->answernumbering));
2090                     $status = fwrite ($bf,end_tag("CALCULATED_OPTIONS",$level,true));
2091                 }
2092             }
2093             $status = question_backup_numerical_options($bf,$preferences,$question,$level);
2095         }
2096         return $status;
2097     }
2099     /// RESTORE FUNCTIONS /////////////////
2101     /*
2102      * Restores the data in the question
2103      *
2104      * This is used in question/restorelib.php
2105      */
2106     function restore($old_question_id,$new_question_id,$info,$restore) {
2107         global $DB;
2109         $status = true;
2111         //Get the calculated-s array
2112         $calculateds = $info['#']['CALCULATED'];
2114         //Iterate over calculateds
2115         for($i = 0; $i < sizeof($calculateds); $i++) {
2116             $cal_info = $calculateds[$i];
2117             //traverse_xmlize($cal_info);                                                                 //Debug
2118             //print_object ($GLOBALS['traverse_array']);                                                  //Debug
2119             //$GLOBALS['traverse_array']="";                                                              //Debug
2121             //Now, build the question_calculated record structure
2122             $calculated->question = $new_question_id;
2123             $calculated->answer = backup_todb($cal_info['#']['ANSWER']['0']['#']);
2124             $calculated->tolerance = backup_todb($cal_info['#']['TOLERANCE']['0']['#']);
2125             $calculated->tolerancetype = backup_todb($cal_info['#']['TOLERANCETYPE']['0']['#']);
2126             $calculated->correctanswerlength = backup_todb($cal_info['#']['CORRECTANSWERLENGTH']['0']['#']);
2127             $calculated->correctanswerformat = backup_todb($cal_info['#']['CORRECTANSWERFORMAT']['0']['#']);
2129             ////We have to recode the answer field
2130             $answer = backup_getid($restore->backup_unique_code,"question_answers",$calculated->answer);
2131             if ($answer) {
2132                 $calculated->answer = $answer->new_id;
2133             }
2135             //The structure is equal to the db, so insert the question_calculated
2136             $newid = $DB->insert_record ("question_calculated",$calculated);
2138             //Do some output
2139             if (($i+1) % 50 == 0) {
2140                 if (!defined('RESTORE_SILENTLY')) {
2141                     echo ".";
2142                     if (($i+1) % 1000 == 0) {
2143                         echo "<br />";
2144                     }
2145                 }
2146                 backup_flush(300);
2147             }
2148             //Get the calculated_options array
2149             // need to check as old questions don't have calculated_options record
2150             if(isset($info['#']['CALCULATED_OPTIONS'])){
2151                 $calculatedoptions = $info['#']['CALCULATED_OPTIONS'];
2153                 //Iterate over calculated_options
2154                 for($i = 0; $i < sizeof($calculatedoptions); $i++){
2155                     $cal_info = $calculatedoptions[$i];
2156                     //traverse_xmlize($cal_info);                                                                 //Debug
2157                     //print_object ($GLOBALS['traverse_array']);                                                  //Debug
2158                     //$GLOBALS['traverse_array']="";                                                              //Debug
2160                     //Now, build the question_calculated_options record structure
2161                     $calculated_options->questionid = $new_question_id;
2162                     $calculated_options->synchronize = backup_todb($cal_info['#']['SYNCHRONIZE']['0']['#']);
2163                     $calculated_options->single = backup_todb($cal_info['#']['SINGLE']['0']['#']);
2164                     $calculated_options->shuffleanswers = isset($cal_info['#']['SHUFFLEANSWERS']['0']['#'])?backup_todb($mul_info['#']['SHUFFLEANSWERS']['0']['#']):'';
2165                     $calculated_options->correctfeedback = backup_todb($cal_info['#']['CORRECTFEEDBACK']['0']['#']);
2166                     $calculated_options->partiallycorrectfeedback = backup_todb($cal_info['#']['PARTIALLYCORRECTFEEDBACK']['0']['#']);
2167                     $calculated_options->incorrectfeedback = backup_todb($cal_info['#']['INCORRECTFEEDBACK']['0']['#']);
2168                     $calculated_options->answernumbering = backup_todb($cal_info['#']['ANSWERNUMBERING']['0']['#']);
2170                     //The structure is equal to the db, so insert the question_calculated_options
2171                     $newid = $DB->insert_record ("question_calculated_options",$calculated_options);
2173                     //Do some output
2174                     if (($i+1) % 50 == 0) {
2175                         if (!defined('RESTORE_SILENTLY')) {
2176                             echo ".";
2177                             if (($i+1) % 1000 == 0) {
2178                                 echo "<br />";
2179                             }
2180                         }
2181                         backup_flush(300);
2182                     }
2183                 }
2184             }
2185             //Now restore numerical_units
2186             $status = question_restore_numerical_units ($old_question_id,$new_question_id,$cal_info,$restore);
2187             $status = question_restore_numerical_options($old_question_id,$new_question_id,$info,$restore);
2188             //Now restore dataset_definitions
2189             if ($status && $newid) {
2190                 $status = question_restore_dataset_definitions ($old_question_id,$new_question_id,$cal_info,$restore);
2191             }
2193             if (!$newid) {
2194                 $status = false;
2195             }
2196         }
2198         return $status;
2199     }
2201     /**
2202      * Runs all the code required to set up and save an essay question for testing purposes.
2203      * Alternate DB table prefix may be used to facilitate data deletion.
2204      */
2205     function generate_test($name, $courseid = null) {
2206         global $DB;
2207         list($form, $question) = parent::generate_test($name, $courseid);
2208         $form->feedback = 1;
2209         $form->multiplier = array(1, 1);
2210         $form->shuffleanswers = 1;
2211         $form->noanswers = 1;
2212         $form->qtype ='calculated';
2213         $question->qtype ='calculated';
2214         $form->answers = array('{a} + {b}');
2215         $form->fraction = array(1);
2216         $form->tolerance = array(0.01);
2217         $form->tolerancetype = array(1);
2218         $form->correctanswerlength = array(2);
2219         $form->correctanswerformat = array(1);
2220         $form->questiontext = "What is {a} + {b}?";
2222         if ($courseid) {
2223             $course = $DB->get_record('course', array('id'=> $courseid));
2224         }
2226         $new_question = $this->save_question($question, $form, $course);
2228         $dataset_form = new stdClass();
2229         $dataset_form->nextpageparam["forceregeneration"]= 1;
2230         $dataset_form->calcmin = array(1 => 1.0, 2 => 1.0);
2231         $dataset_form->calcmax = array(1 => 10.0, 2 => 10.0);
2232         $dataset_form->calclength = array(1 => 1, 2 => 1);
2233         $dataset_form->number = array(1 => 5.4 , 2 => 4.9);
2234         $dataset_form->itemid = array(1 => '' , 2 => '');
2235         $dataset_form->calcdistribution = array(1 => 'uniform', 2 => 'uniform');
2236         $dataset_form->definition = array(1 => "1-0-a",
2237             2 => "1-0-b");
2238         $dataset_form->nextpageparam = array('forceregeneration' => false);
2239         $dataset_form->addbutton = 1;
2240         $dataset_form->selectadd = 1;
2241         $dataset_form->courseid = $courseid;
2242         $dataset_form->cmid = 0;
2243         $dataset_form->id = $new_question->id;
2244         $this->save_dataset_items($new_question, $dataset_form);
2246         return $new_question;
2247     }
2249     /**
2250      * When move the category of questions, the belonging files should be moved as well
2251      * @param object $question, question information
2252      * @param object $newcategory, target category information
2253      */
2254     function move_files($question, $newcategory) {
2255         global $DB;
2256         parent::move_files($question, $newcategory);
2258         $fs = get_file_storage();
2259         // process files in answer
2260         if (!$oldanswers = $DB->get_records('question_answers', array('question' =>  $question->id), 'id ASC')) {
2261             $oldanswers = array();
2262         }
2263         $component = 'question';
2264         $filearea = 'answerfeedback';
2265         foreach ($oldanswers as $answer) {
2266             $files = $fs->get_area_files($question->contextid, $component, $filearea, $answer->id);
2267             foreach ($files as $storedfile) {
2268                 if (!$storedfile->is_directory()) {
2269                     $newfile = new object();
2270                     $newfile->contextid = (int)$newcategory->contextid;
2271                     $fs->create_file_from_storedfile($newfile, $storedfile);
2272                     $storedfile->delete();
2273                 }
2274             }
2275         }
2276         $component = 'qtype_numerical';
2277         $filearea = 'instruction';
2278         $files = $fs->get_area_files($question->contextid, $component, $filearea, $question->id);
2279         foreach ($files as $storedfile) {
2280             if (!$storedfile->is_directory()) {
2281                 $newfile = new object();
2282                 $newfile->contextid = (int)$newcategory->contextid;
2283                 $fs->create_file_from_storedfile($newfile, $storedfile);
2284                 $storedfile->delete();
2285             }
2286         }
2287     }
2289     function check_file_access($question, $state, $options, $contextid, $component,
2290             $filearea, $args) {
2291         $itemid = reset($args);
2292         if ($component == 'question' && $filearea == 'answerfeedback') {
2294             // check if answer id exists
2295             $result = $options->feedback && array_key_exists($itemid, $question->options->answers);
2296             if (!$result) {
2297                 return false;
2298             }
2299             // check response
2300             if (!$this->check_response($question, $state)) {
2301                 return false;
2302             }
2303             return true;
2304         } else if ($filearea == 'instruction') {
2305             // TODO: should it be display all the time like questiontext?
2306             // check if question id exists
2307             if ($itemid != $question->id) {
2308                 return false;
2309             } else {
2310                 return true;
2311             }
2312         } else if (in_array($filearea, array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) {
2313             // TODO: calculated type doesn't display question feedback yet
2314             return false;
2315         } else {
2316             return parent::check_file_access($question, $state, $options, $contextid, $component,
2317                     $filearea, $args);
2318         }
2319     }
2321 //// END OF CLASS ////
2323 //////////////////////////////////////////////////////////////////////////
2324 //// INITIATION - Without this line the question type is not in use... ///
2325 //////////////////////////////////////////////////////////////////////////
2326 question_register_questiontype(new question_calculated_qtype());
2328 if ( ! defined ("CALCULATEDQUESTIONMAXITEMNUMBER")) {
2329     define("CALCULATEDQUESTIONMAXITEMNUMBER", 100);
2332 function qtype_calculated_calculate_answer($formula, $individualdata,
2333     $tolerance, $tolerancetype, $answerlength, $answerformat='1', $unit='') {
2334     /// The return value has these properties:
2335     /// ->answer    the correct answer
2336     /// ->min       the lower bound for an acceptable response
2337     /// ->max       the upper bound for an accetpable response
2339     /// Exchange formula variables with the correct values...
2340     global $QTYPES;
2341     $answer = $QTYPES['calculated']->substitute_variables_and_eval($formula, $individualdata);
2342     if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */
2343         /*** Adjust to the correct number of decimals ***/
2344         if (stripos($answer,'e')>0 ){
2345             $answerlengthadd = strlen($answer)-stripos($answer,'e');
2346         }else {
2347             $answerlengthadd = 0 ;
2348         }
2349         $calculated->answer = round(floatval($answer), $answerlength+$answerlengthadd);
2351         if ($answerlength) {
2352             /* Try to include missing zeros at the end */
2354             if (preg_match('~^(.*\\.)(.*)$~', $calculated->answer, $regs)) {
2355                 $calculated->answer = $regs[1] . substr(
2356                     $regs[2] . '00000000000000000000000000000000000000000x',
2357                     0, $answerlength)
2358                     . $unit;
2359             } else {
2360                 $calculated->answer .=
2361                     substr('.00000000000000000000000000000000000000000x',
2362                         0, $answerlength + 1) . $unit;
2363             }
2364         } else {
2365             /* Attach unit */
2366             $calculated->answer .= $unit;
2367         }
2369     } else if ($answer) { // Significant figures does only apply if the result is non-zero
2371         // Convert to positive answer...
2372         if ($answer < 0) {
2373             $answer = -$answer;
2374             $sign = '-';
2375         } else {
2376             $sign = '';
2377         }
2379         // Determine the format 0.[1-9][0-9]* for the answer...
2380         $p10 = 0;
2381         while ($answer < 1) {
2382             --$p10;
2383             $answer *= 10;
2384         }
2385         while ($answer >= 1) {
2386             ++$p10;
2387             $answer /= 10;
2388         }
2389         // ... and have the answer rounded of to the correct length
2390         $answer = round($answer, $answerlength);
2392         // Have the answer written on a suitable format,
2393         // Either scientific or plain numeric
2394         if (-2 > $p10 || 4 < $p10) {
2395             // Use scientific format:
2396             $eX = 'e'.--$p10;
2397             $answer *= 10;
2398             if (1 == $answerlength) {
2399                 $calculated->answer = $sign.$answer.$eX.$unit;
2400             } else {
2401                 // Attach additional zeros at the end of $answer,
2402                 $answer .= (1==strlen($answer) ? '.' : '')
2403                     . '00000000000000000000000000000000000000000x';
2404                 $calculated->answer = $sign
2405                     .substr($answer, 0, $answerlength +1).$eX.$unit;
2406             }
2407         } else {
2408             // Stick to plain numeric format
2409             $answer *= "1e$p10";
2410             if (0.1 <= $answer / "1e$answerlength") {
2411                 $calculated->answer = $sign.$answer.$unit;
2412             } else {
2413                 // Could be an idea to add some zeros here
2414                 $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '')
2415                     . '00000000000000000000000000000000000000000x';
2416                 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
2417                 $calculated->answer = $sign.substr($answer, 0, $oklen).$unit;
2418             }
2419         }
2421     } else {
2422         $calculated->answer = 0.0;
2423     }
2425     /// Return the result
2426     return $calculated;
2430 function qtype_calculated_find_formula_errors($formula) {
2431     /// Validates the formula submitted from the question edit page.
2432     /// Returns false if everything is alright.
2433     /// Otherwise it constructs an error message
2434     // Strip away dataset names
2435     while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) {
2436         $formula = str_replace($regs[0], '1', $formula);
2437     }
2439     // Strip away empty space and lowercase it
2440     $formula = strtolower(str_replace(' ', '', $formula));
2442     $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */
2443     $operatorornumber = "[$safeoperatorchar.0-9eE]";
2445     while ( preg_match("~(^|[$safeoperatorchar,(])([a-z0-9_]*)\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)~",
2446         $formula, $regs)) {
2447         switch ($regs[2]) {
2448             // Simple parenthesis
2449         case '':
2450             if ($regs[4] || strlen($regs[3])==0) {
2451                 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
2452             }
2453             break;
2455             // Zero argument functions
2456         case 'pi':
2457             if ($regs[3]) {
2458                 return get_string('functiontakesnoargs', 'quiz', $regs[2]);
2459             }
2460             break;
2462             // Single argument functions (the most common case)
2463         case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
2464         case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
2465         case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
2466         case 'exp': case 'expm1': case 'floor': case 'is_finite':
2467         case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
2468         case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
2469         case 'tan': case 'tanh':
2470             if (!empty($regs[4]) || empty($regs[3])) {
2471                 return get_string('functiontakesonearg','quiz',$regs[2]);
2472             }
2473             break;
2475             // Functions that take one or two arguments
2476         case 'log': case 'round':
2477             if (!empty($regs[5]) || empty($regs[3])) {
2478                 return get_string('functiontakesoneortwoargs','quiz',$regs[2]);
2479             }
2480             break;
2482             // Functions that must have two arguments
2483         case 'atan2': case 'fmod': case 'pow':
2484             if (!empty($regs[5]) || empty($regs[4])) {
2485                 return get_string('functiontakestwoargs', 'quiz', $regs[2]);
2486             }
2487             break;
2489             // Functions that take two or more arguments
2490         case 'min': case 'max':
2491             if (empty($regs[4])) {
2492                 return get_string('functiontakesatleasttwo','quiz',$regs[2]);
2493             }
2494             break;
2496         default:
2497             return get_string('unsupportedformulafunction','quiz',$regs[2]);
2498         }
2500         // Exchange the function call with '1' and then chack for
2501         // another function call...
2502         if ($regs[1]) {
2503             // The function call is proceeded by an operator
2504             $formula = str_replace($regs[0], $regs[1] . '1', $formula);
2505         } else {
2506             // The function call starts the formula
2507             $formula = preg_replace("~^$regs[2]\\([^)]*\\)~", '1', $formula);
2508         }
2509     }
2511     if (preg_match("~[^$safeoperatorchar.0-9eE]+~", $formula, $regs)) {
2512         return get_string('illegalformulasyntax', 'quiz', $regs[0]);
2513     } else {
2514         // Formula just might be valid
2515         return false;
2516     }