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