MDL-10110 Creating the calculatedmulti question type
[moodle.git] / question / type / calculatedmulti / questiontype.php
CommitLineData
2d279432
PP
1<?php
2
3/////////////////
4// CALCULATED ///
5/////////////////
6
7/// QUESTION TYPE CLASS //////////////////
8
9
10
11class question_calculatedmulti_qtype extends question_calculated_qtype {
12
13 // Used by the function custom_generator_tools:
14 var $calcgenerateidhasbeenadded = false;
15 public $virtualqtype = false;
16
17 function name() {
18 return 'calculatedmulti';
19 }
20
21 function has_wildcards_in_responses($question, $subqid) {
22 return true;
23 }
24
25 function requires_qtypes() {
26 return array('multichoice');
27 }
28/*
29 function get_question_options(&$question) {
30 // First get the datasets and default options
31 global $CFG, $DB, $OUTPUT, $QTYPES;
32 if (!$question->options = $DB->get_record('question_calculated_options', array('question' => $question->id))) {
33 // echo $OUTPUT->notification('Error: Missing question options for calculated question'.$question->id.'!');
34 // return false;
35 $question->options->synchronize = 0;
36 // $question->options->multichoice = 1;
37
38 }
39 // echo "<p> questionoptions <pre>";print_r($question);echo "</pre></p>";
40 // $QTYPES['numerical']->get_numerical_options($question);
41 /* $question->options->unitgradingtype = 0;
42 $question->options->unitpenalty = 0;
43 $question->options->showunits = 0 ;
44 $question->options->unitsleft = 0 ;
45 $question->options->instructions = '' ;
46 // echo "<p> questionoptions <pre>";print_r($question);echo "</pre></p>";
47
48 if (!$question->options->answers = $DB->get_records_sql(
49 "SELECT a.*, c.tolerance, c.tolerancetype, c.correctanswerlength, c.correctanswerformat " .
50 "FROM {question_answers} a, " .
51 " {question_calculated} c " .
52 "WHERE a.question = ? " .
53 "AND a.id = c.answer ".
54 "ORDER BY a.id ASC", array($question->id))) {
55 echo $OUTPUT->notification('Error: Missing question answer for calculated question ' . $question->id . '!');
56 return false;
57 }
58
59
60 if(false === parent::get_question_options($question)) {
61 return false;
62 }
63
64 if (!$options = $DB->get_records('question_calculated', array('question' => $question->id))) {
65 notify("No options were found for calculated question
66 #{$question->id}! Proceeding with defaults.");
67 // $options = new Array();
68 $options= new stdClass;
69 $options->tolerance = 0.01;
70 $options->tolerancetype = 1; // relative
71 $options->correctanswerlength = 2;
72 $options->correctanswerformat = 1; // decimals
73 }
74
75 // For historic reasons we also need these fields in the answer objects.
76 // This should eventually be removed and related code changed to use
77 // the values in $question->options instead.
78 foreach ($question->options->answers as $key => $answer) {
79 $answer = &$question->options->answers[$key]; // for PHP 4.x
80 $answer->calcid = $options->id;
81 $answer->tolerance = $options->tolerance;
82 $answer->tolerancetype = $options->tolerancetype;
83 $answer->correctanswerlength = $options->correctanswerlength;
84 $answer->correctanswerformat = $options->correctanswerformat;
85 }
86
87 //$virtualqtype = $this->get_virtual_qtype( $question);
88 // $QTYPES['numerical']->get_numerical_units($question);
89
90 if( isset($question->export_process)&&$question->export_process){
91 $question->options->datasets = $this->get_datasets_for_export($question);
92 }
93 return true;
94 }
95*/
96
97 function save_question_options($question) {
98 //$options = $question->subtypeoptions;
99 // Get old answers:
100 global $CFG, $DB, $QTYPES ;
101 if (isset($question->answer) && !isset($question->answers)) {
102 $question->answers = $question->answer;
103 }
104 // calculated options
105 $update = true ;
106 $options = $DB->get_record("question_calculated_options", array("question" => $question->id));
107 if (!$options) {
108 $update = false;
109 $options = new stdClass;
110 $options->question = $question->id;
111 }
112 $options->synchronize = $question->synchronize;
113 // $options->multichoice = $question->multichoice;
114 $options->single = $question->single;
115 $options->answernumbering = $question->answernumbering;
116 $options->shuffleanswers = $question->shuffleanswers;
117 $options->correctfeedback = trim($question->correctfeedback);
118 $options->partiallycorrectfeedback = trim($question->partiallycorrectfeedback);
119 $options->incorrectfeedback = trim($question->incorrectfeedback);
120 if ($update) {
121 if (!$DB->update_record("question_calculated_options", $options)) {
122 $result->error = "Could not update calculated question options! (id=$options->id)";
123 return $result;
124 }
125 } else {
126 if (!$DB->insert_record("question_calculated_options", $options)) {
127 $result->error = "Could not insert calculated question options!";
128 return $result;
129 }
130 }
131
132 // Get old versions of the objects
133 if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) {
134 $oldanswers = array();
135 }
136
137 if (!$oldoptions = $DB->get_records('question_calculated', array('question' => $question->id), 'answer ASC')) {
138 $oldoptions = array();
139 }
140
141 // Save the units.
142 $virtualqtype = $this->get_virtual_qtype( $question);
143 // $result = $virtualqtype->save_numerical_units($question);
144 if (isset($result->error)) {
145 return $result;
146 } else {
147 $units = &$result->units;
148 }
149 // Insert all the new answers
150 if (isset($question->answer) && !isset($question->answers)) {
151 $question->answers=$question->answer;
152 }
153 foreach ($question->answers as $key => $dataanswer) {
154 if ( trim($dataanswer) != '' ) {
155 $answer = new stdClass;
156 $answer->question = $question->id;
157 $answer->answer = trim($dataanswer);
158 $answer->fraction = $question->fraction[$key];
159 $answer->feedback = trim($question->feedback[$key]);
160
161 if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it
162 $answer->id = $oldanswer->id;
163 $DB->update_record("question_answers", $answer);
164 } else { // This is a completely new answer
165 $answer->id = $DB->insert_record("question_answers", $answer);
166 }
167
168 // Set up the options object
169 if (!$options = array_shift($oldoptions)) {
170 $options = new stdClass;
171 }
172 $options->question = $question->id;
173 $options->answer = $answer->id;
174 $options->tolerance = trim($question->tolerance[$key]);
175 $options->tolerancetype = trim($question->tolerancetype[$key]);
176 $options->correctanswerlength = trim($question->correctanswerlength[$key]);
177 $options->correctanswerformat = trim($question->correctanswerformat[$key]);
178
179 // Save options
180 if (isset($options->id)) { // reusing existing record
181 $DB->update_record('question_calculated', $options);
182 } else { // new options
183 $DB->insert_record('question_calculated', $options);
184 }
185 }
186 }
187 // delete old answer records
188 if (!empty($oldanswers)) {
189 foreach($oldanswers as $oa) {
190 $DB->delete_records('question_answers', array('id' => $oa->id));
191 }
192 }
193
194 // delete old answer records
195 if (!empty($oldoptions)) {
196 foreach($oldoptions as $oo) {
197 $DB->delete_records('question_calculated', array('id' => $oo->id));
198 }
199 }
200 // $result = $QTYPES['numerical']->save_numerical_options($question);
201 // if (isset($result->error)) {
202 // return $result;
203 // }
204
205
206 if( isset($question->import_process)&&$question->import_process){
207 $this->import_datasets($question);
208 }
209 // Report any problems.
210 if (!empty($result->notice)) {
211 return $result;
212 }
213 return true;
214 }
215
216 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
217 // Find out how many datasets are available
218 global $CFG, $DB, $QTYPES;
219 if(!$maxnumber = (int)$DB->get_field_sql(
220 "SELECT MIN(a.itemcount)
221 FROM {question_dataset_definitions} a,
222 {question_datasets} b
223 WHERE b.question = ?
224 AND a.id = b.datasetdefinition", array($question->id))) {
225 print_error('cannotgetdsforquestion', 'question', '', $question->id);
226 }
227 $sql = "SELECT i.*
228 FROM {question_datasets} d,
229 {question_dataset_definitions} i
230 WHERE d.question = ?
231 AND d.datasetdefinition = i.id
232 AND i.category != 0
233 ";
234 if (!$question->options->synchronize || !$records = $DB->get_records_sql($sql, array($question->id))) {
235 $synchronize_calculated = false ;
236 }else {
237 $synchronize_calculated = true ;
238 }
239
240 // Choose a random dataset
241 if ( $synchronize_calculated === false ) {
242 $state->options->datasetitem = rand(1, $maxnumber);
243 }else{
244 $state->options->datasetitem = intval( $maxnumber * substr($attempt->timestart,-2) /100 ) ;
245 if ($state->options->datasetitem < 1) {
246 $state->options->datasetitem =1 ;
247 } else if ($state->options->datasetitem > $maxnumber){
248 $state->options->datasetitem = $maxnumber ;
249 }
250
251 };
252 $state->options->dataset =
253 $this->pick_question_dataset($question,$state->options->datasetitem);
254 // create an array of answerids ??? why so complicated ???
255 $answerids = array_values(array_map(create_function('$val',
256 'return $val->id;'), $question->options->answers));
257 // Shuffle the answers if required
258 if (!empty($cmoptions->shuffleanswers) and !empty($question->options->shuffleanswers)) {
259 $answerids = swapshuffle($answerids);
260 }
261 $state->options->order = $answerids;
262 // Create empty responses
263 if ($question->options->single) {
264 $state->responses = array('' => '');
265 } else {
266 $state->responses = array();
267 }
268 return true;
269
270 }
271
272 function save_session_and_responses(&$question, &$state) {
273 global $DB;
274 $responses = 'dataset'.$state->options->datasetitem.'-' ;
275 $responses .= implode(',', $state->options->order) . ':';
276 $responses .= implode(',', $state->responses);
277
278 // Set the legacy answer field
279 if (!$DB->set_field('question_states', 'answer', $responses, array('id'=> $state->id))) {
280 return false;
281 }
282 return true;
283 }
284
285 function create_runtime_question($question, $form) {
286 $question = parent::create_runtime_question($question, $form);
287 $question->options->answers = array();
288 foreach ($form->answers as $key => $answer) {
289 $a->answer = trim($form->answer[$key]);
290 $a->fraction = $form->fraction[$key];//new
291 $a->tolerance = $form->tolerance[$key];
292 $a->tolerancetype = $form->tolerancetype[$key];
293 $a->correctanswerlength = $form->correctanswerlength[$key];
294 $a->correctanswerformat = $form->correctanswerformat[$key];
295 $question->options->answers[] = clone($a);
296 }
297
298 return $question;
299 }
300
301 function validate_form($form) {
302 switch($form->wizardpage) {
303 case 'question':
304 $calculatedmessages = array();
305 if (empty($form->name)) {
306 $calculatedmessages[] = get_string('missingname', 'quiz');
307 }
308 if (empty($form->questiontext)) {
309 $calculatedmessages[] = get_string('missingquestiontext', 'quiz');
310 }
311 // Verify formulas
312 foreach ($form->answers as $key => $answer) {
313 if ('' === trim($answer)) {
314 $calculatedmessages[] =
315 get_string('missingformula', 'quiz');
316 }
317 if ($formulaerrors =
318 qtype_calculated_find_formula_errors($answer)) {
319 $calculatedmessages[] = $formulaerrors;
320 }
321 if (! isset($form->tolerance[$key])) {
322 $form->tolerance[$key] = 0.0;
323 }
324 if (! is_numeric($form->tolerance[$key])) {
325 $calculatedmessages[] =
326 get_string('tolerancemustbenumeric', 'quiz');
327 }
328 }
329
330 if (!empty($calculatedmessages)) {
331 $errorstring = "The following errors were found:<br />";
332 foreach ($calculatedmessages as $msg) {
333 $errorstring .= $msg . '<br />';
334 }
335 print_error($errorstring);
336 }
337
338 break;
339 default:
340 return parent::validate_form($form);
341 break;
342 }
343 return true;
344 }
345 function finished_edit_wizard(&$form) {
346 return isset($form->backtoquiz);
347 }
348 // This gets called by editquestion.php after the standard question is saved
349 function print_next_wizard_page(&$question, &$form, $course) {
350 global $CFG, $USER, $SESSION, $COURSE;
351
352 // Catch invalid navigation & reloads
353 if (empty($question->id) && empty($SESSION->calculated)) {
354 redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired.', 3);
355 }
356
357 // See where we're coming from
358 switch($form->wizardpage) {
359 case 'question':
360 require("$CFG->dirroot/question/type/calculated/datasetdefinitions.php");
361 break;
362 case 'datasetdefinitions':
363 case 'datasetitems':
364 require("$CFG->dirroot/question/type/calculated/datasetitems.php");
365 break;
366 default:
367 print_error('invalidwizardpage', 'question');
368 break;
369 }
370 }
371
372 // This gets called by question2.php after the standard question is saved
373 function &next_wizard_form($submiturl, $question, $wizardnow){
374 global $CFG, $SESSION, $COURSE;
375
376 // Catch invalid navigation & reloads
377 if (empty($question->id) && empty($SESSION->calculated)) {
378 redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired. Cannot get next wizard form.', 3);
379 }
380 if (empty($question->id)){
381 $question =& $SESSION->calculated->questionform;
382 }
383
384 // See where we're coming from
385 switch($wizardnow) {
386 case 'datasetdefinitions':
387 require("$CFG->dirroot/question/type/calculated/datasetdefinitions_form.php");
388 $mform =& new question_dataset_dependent_definitions_form("$submiturl?wizardnow=datasetdefinitions", $question);
389 break;
390 case 'datasetitems':
391 require("$CFG->dirroot/question/type/calculated/datasetitems_form.php");
392 $regenerate = optional_param('forceregeneration', 0, PARAM_BOOL);
393 $mform =& new question_dataset_dependent_items_form("$submiturl?wizardnow=datasetitems", $question, $regenerate);
394 break;
395 default:
396 print_error('invalidwizardpage', 'question');
397 break;
398 }
399
400 return $mform;
401 }
402
403 /**
404 * This method should be overriden if you want to include a special heading or some other
405 * html on a question editing page besides the question editing form.
406 *
407 * @param question_edit_form $mform a child of question_edit_form
408 * @param object $question
409 * @param string $wizardnow is '' for first page.
410 */
411 function display_question_editing_page(&$mform, $question, $wizardnow){
412 global $OUTPUT ;
413 switch ($wizardnow){
414 case '':
415 //on first page default display is fine
416 parent::display_question_editing_page($mform, $question, $wizardnow);
417 return;
418 break;
419 case 'datasetdefinitions':
420 echo $OUTPUT->heading_with_help(get_string("choosedatasetproperties", "quiz"), 'questiondatasets', 'quiz');
421
422 /* $heading = get_string("question", "quiz").": ".$question->name;
423 $helpicon = new moodle_help_icon();
424 $helpicon->text = get_string("choosedatasetproperties", "quiz");
425 $helpicon->page = 'questiondatasets';
426 $helpicon->module = 'quiz';
427 echo $OUTPUT->heading($heading);
428 echo $OUTPUT->heading_with_help($helpicon);*/
429 break;
430 case 'datasetitems':
431 echo $OUTPUT->heading_with_help(get_string("editdatasets", "quiz"), 'questiondatasets', 'quiz');
432
433 /* $heading = get_string("question", "quiz").": ".$question->name;
434 $helpicon = new moodle_help_icon();
435 $helpicon->text = get_string("editdatasets", "quiz");
436 $helpicon->page = 'questiondatasets';
437 $helpicon->module = 'quiz';
438 echo $OUTPUT->heading($heading);
439 echo $OUTPUT->heading_with_help($helpicon);*/
440 break;
441 }
442
443
444 $mform->display();
445
446 }
447
448 /**
449 * This method prepare the $datasets in a format similar to dadatesetdefinitions_form.php
450 * so that they can be saved
451 * using the function save_dataset_definitions($form)
452 * when creating a new calculated question or
453 * whenediting an already existing calculated question
454 * or by function save_as_new_dataset_definitions($form, $initialid)
455 * when saving as new an already existing calculated question
456 *
457 * @param object $form
458 * @param int $questionfromid default = '0'
459 */
460 function preparedatasets(&$form , $questionfromid='0'){
461 // the dataset names present in the edit_question_form and edit_calculated_form are retrieved
462 $possibledatasets = $this->find_dataset_names($form->questiontext);
463 $mandatorydatasets = array();
464 foreach ($form->answers as $answer) {
465 $mandatorydatasets += $this->find_dataset_names($answer);
466 }
467 // if there are identical datasetdefs already saved in the original question.
468 // either when editing a question or saving as new
469 // they are retrieved using $questionfromid
470 if ($questionfromid!='0'){
471 $form->id = $questionfromid ;
472 }
473 $datasets = array();
474 $key = 0 ;
475 // always prepare the mandatorydatasets present in the answers
476 // the $options are not used here
477 foreach ($mandatorydatasets as $datasetname) {
478 if (!isset($datasets[$datasetname])) {
479 list($options, $selected) =
480 $this->dataset_options($form, $datasetname);
481 $datasets[$datasetname]='';
482 $form->dataset[$key]=$selected ;
483 $key++;
484 }
485 }
486 // do not prepare possibledatasets when creating a question
487 // they will defined and stored with datasetdefinitions_form.php
488 // the $options are not used here
489 if ($questionfromid!='0'){
490
491 foreach ($possibledatasets as $datasetname) {
492 if (!isset($datasets[$datasetname])) {
493 list($options, $selected) =
494 $this->dataset_options($form, $datasetname,false);
495 $datasets[$datasetname]='';
496 $form->dataset[$key]=$selected ;
497 $key++;
498 }
499 }
500 }
501 return $datasets ;
502 }
503
504 /**
505 * this version save the available data at the different steps of the question editing process
506 * without using global $SESSION as storage between steps
507 * at the first step $wizardnow = 'question'
508 * when creating a new question
509 * when modifying a question
510 * when copying as a new question
511 * the general parameters and answers are saved using parent::save_question
512 * then the datasets are prepared and saved
513 * at the second step $wizardnow = 'datasetdefinitions'
514 * the datadefs final type are defined as private, category or not a datadef
515 * at the third step $wizardnow = 'datasetitems'
516 * the datadefs parameters and the data items are created or defined
517 *
518 * @param object question
519 * @param object $form
520 * @param int $course
521 * @param PARAM_ALPHA $wizardnow should be added as we are coming from question2.php
522 */
523 function convert_answers (&$question, &$state){
524 foreach ($question->options->answers as $key => $answer) {
525 $answer->answer = $this->substitute_variables($answer->answer, $state->options->dataset);
526 //evaluate the equations i.e {=5+4)
527 $qtext = "";
528 $qtextremaining = $answer->answer ;
529 // while (preg_match('~\{(=)|%[[:digit]]\.=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
530 while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
531
532 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
533 $qtext =$qtext.$qtextsplits[0];
534 $qtextremaining = $qtextsplits[1];
535 if (empty($regs1[1])) {
536 $str = '';
537 } else {
538 if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
539 $str=$formulaerrors ;
540 }else {
541 eval('$str = '.$regs1[1].';');
542 $texteval= qtype_calculated_calculate_answer(
543 $str, $state->options->dataset, $answer->tolerance,
544 $answer->tolerancetype, $answer->correctanswerlength,
545 $answer->correctanswerformat, '');
546 $str = $texteval->answer;
547 }
548 }
549 $qtext = $qtext.$regs1[0].$str ;
550 }
551 $answer->answer = $qtext.$qtextremaining ; ;
552 }
553 }
554
555 function get_default_numerical_unit($question,$virtualqtype){
556 $unit = '';
557 return $unit ;
558 }
559 function grade_responses(&$question, &$state, $cmoptions) {
560 // Forward the grading to the virtual qtype
561 // We modify the question to look like a multichoice question
562 // for grading nothing to do
563/* $numericalquestion = fullclone($question);
564 foreach ($numericalquestion->options->answers as $key => $answer) {
565 $answer = $numericalquestion->options->answers[$key]->answer; // for PHP 4.x
566 $numericalquestion->options->answers[$key]->answer = $this->substitute_variables_and_eval($answer,
567 $state->options->dataset);
568 }*/
569 $virtualqtype = $this->get_virtual_qtype( $question);
570 return $virtualqtype->grade_responses($question, $state, $cmoptions) ;
571 }
572
573 function response_summary($question, $state, $length=80, $formatting=true) {
574 // The actual response is the bit after the hyphen
575 return substr($state->answer, strpos($state->answer, '-')+1, $length);
576 }
577
578 // ULPGC ecastro
579 function check_response(&$question, &$state) {
580 // Forward the checking to the virtual qtype
581 // We modify the question to look like a numerical question
582 $numericalquestion = clone($question);
583 $numericalquestion->options = clone($question->options);
584 foreach ($question->options->answers as $key => $answer) {
585 $numericalquestion->options->answers[$key] = clone($answer);
586 }
587 foreach ($numericalquestion->options->answers as $key => $answer) {
588 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
589 $answer->answer = $this->substitute_variables_and_eval($answer->answer,
590 $state->options->dataset);
591 }
592 $virtualqtype = $this->get_virtual_qtype( $question);
593 return $virtualqtype->check_response($numericalquestion, $state) ;
594 }
595
596 // ULPGC ecastro
597 function get_actual_response(&$question, &$state) {
598 // Substitute variables in questiontext before giving the data to the
599 // virtual type
600 $virtualqtype = $this->get_virtual_qtype( $question);
601 $unit = '' ;//$virtualqtype->get_default_numerical_unit($question);
602
603 // We modify the question to look like a multichoice question
604 $numericalquestion = clone($question);
605 $this->convert_answers ($numericalquestion, $state);
606 $this->convert_questiontext ($numericalquestion, $state);
607 /* $numericalquestion->questiontext = $this->substitute_variables_and_eval(
608 $numericalquestion->questiontext, $state->options->dataset);*/
609 $responses = $virtualqtype->get_all_responses($numericalquestion, $state);
610 $response = reset($responses->responses);
611 $correct = $response->answer.' : ';
612
613 $responses = $virtualqtype->get_actual_response($numericalquestion, $state);
614
615 foreach ($responses as $key=>$response){
616 $responses[$key] = $correct.$response;
617 }
618
619 return $responses;
620 }
621
622 function create_virtual_qtype() {
623 global $CFG;
624 require_once("$CFG->dirroot/question/type/multichoice/questiontype.php");
625 return new question_multichoice_qtype();
626 }
627
628
629 function comment_header($question) {
630 //$this->get_question_options($question);
631 $strheader = '';
632 $delimiter = '';
633
634 $answers = $question->options->answers;
635
636 foreach ($answers as $key => $answer) {
637 if (is_string($answer)) {
638 $strheader .= $delimiter.$answer;
639 } else {
640 $strheader .= $delimiter.$answer->answer;
641 }
642 $delimiter = '<br/>';
643 }
644 return $strheader;
645 }
646
647 function comment_on_datasetitems($qtypeobj,$questionid,$questiontext, $answers,$data, $number) { //multichoice_
648 global $DB;
649 $comment = new stdClass;
650 $comment->stranswers = array();
651 $comment->outsidelimit = false ;
652 $comment->answers = array();
653 /// Find a default unit:
654 /* if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units', array('question'=> $questionid, 'multiplier' => 1.0))) {
655 $unit = $unit->unit;
656 } else {
657 $unit = '';
658 }*/
659
660 $answers = fullclone($answers);
661 $strmin = get_string('min', 'quiz');
662 $strmax = get_string('max', 'quiz');
663 $errors = '';
664 $delimiter = ': ';
665 foreach ($answers as $key => $answer) {
666 $answer->answer = $this->substitute_variables($answer->answer, $data);
667 //evaluate the equations i.e {=5+4)
668 $qtext = "";
669 $qtextremaining = $answer->answer ;
670 while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
671 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
672 $qtext =$qtext.$qtextsplits[0];
673 $qtextremaining = $qtextsplits[1];
674 if (empty($regs1[1])) {
675 $str = '';
676 } else {
677 if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
678 $str=$formulaerrors ;
679 }else {
680 eval('$str = '.$regs1[1].';');
681 }
682 }
683 $qtext = $qtext.$str ;
684 }
685 $answer->answer = $qtext.$qtextremaining ; ;
686 $comment->stranswers[$key]= $answer->answer ;
687
688
689 /* $formula = $this->substitute_variables($answer->answer,$data);
690 $formattedanswer = qtype_calculated_calculate_answer(
691 $answer->answer, $data, $answer->tolerance,
692 $answer->tolerancetype, $answer->correctanswerlength,
693 $answer->correctanswerformat, $unit);
694 if ( $formula === '*'){
695 $answer->min = ' ';
696 $formattedanswer->answer = $answer->answer ;
697 }else {
698 eval('$answer->answer = '.$formula.';') ;
699 $virtualqtype->get_tolerance_interval($answer);
700 }
701 if ($answer->min === '') {
702 // This should mean that something is wrong
703 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
704 } else if ($formula === '*'){
705 $comment->stranswers[$key] = $formula.' = '.get_string('anyvalue','qtype_calculated').'<br/><br/><br/>';
706 }else{
707 $comment->stranswers[$key]= $formula.' = '.$formattedanswer->answer.'<br/>' ;
708 $comment->stranswers[$key] .= $strmin. $delimiter.$answer->min.'---';
709 $comment->stranswers[$key] .= $strmax.$delimiter.$answer->max;
710 $comment->stranswers[$key] .='<br/>';
711 $correcttrue->correct = $formattedanswer->answer ;
712 $correcttrue->true = $answer->answer ;
713 if ($formattedanswer->answer < $answer->min || $formattedanswer->answer > $answer->max){
714 $comment->outsidelimit = true ;
715 $comment->answers[$key] = $key;
716 $comment->stranswers[$key] .=get_string('trueansweroutsidelimits','qtype_calculated',$correcttrue);//<span class="error">ERROR True answer '..' outside limits</span>';
717 }else {
718 $comment->stranswers[$key] .=get_string('trueanswerinsidelimits','qtype_calculated',$correcttrue);//' True answer :'.$calculated->trueanswer.' inside limits';
719 }
720 $comment->stranswers[$key] .='';
721 }*/
722 }
723 return fullclone($comment);
724 }
725
726
727
728
729
730 function get_correct_responses1(&$question, &$state) {
731 $virtualqtype = $this->get_virtual_qtype( $question);
732 /* if ($question->options->multichoice != 1 ) {
733 if($unit = $virtualqtype->get_default_numerical_unit($question)){
734 $unit = $unit->unit;
735 } else {
736 $unit = '';
737 }
738 foreach ($question->options->answers as $answer) {
739 if (((int) $answer->fraction) === 1) {
740 $answernumerical = qtype_calculated_calculate_answer(
741 $answer->answer, $state->options->dataset, $answer->tolerance,
742 $answer->tolerancetype, $answer->correctanswerlength,
743 $answer->correctanswerformat, ''); // remove unit
744 $correct = array('' => $answernumerical->answer);
745 $correct['answer']= $correct[''];
746 if (isset($correct['']) && $correct[''] != '*' && $unit ) {
747 $correct[''] .= ' '.$unit;
748 $correct['unit']= $unit;
749 }
750 return $correct;
751 }
752 }
753 }else{**/
754 return $virtualqtype->get_correct_responses($question, $state) ;
755 // }
756 return null;
757 }
758
759 function substitute_variables($str, $dataset) {
760 // testing for wrong numerical values
761 // all calculations used this function so testing here should be OK
762
763 foreach ($dataset as $name => $value) {
764 $val = $value ;
765 if(! is_numeric($val)){
766 $a = new stdClass;
767 $a->name = '{'.$name.'}' ;
768 $a->value = $value ;
769 echo $OUTPUT->notification(get_string('notvalidnumber','qtype_calculated',$a));
770 $val = 1.0 ;
771 }
772 if($val < 0 ){
773 $str = str_replace('{'.$name.'}', '('.$val.')', $str);
774 } else {
775 $str = str_replace('{'.$name.'}', $val, $str);
776 }
777 }
778 return $str;
779 }
780 function evaluate_equations($str, $dataset){
781 $formula = $this->substitute_variables($str, $dataset) ;
782 if ($error = qtype_calculated_find_formula_errors($formula)) {
783 return $error;
784 }
785 return $str;
786 }
787
788
789 function substitute_variables_and_eval($str, $dataset) {
790 $formula = $this->substitute_variables($str, $dataset) ;
791 if ($error = qtype_calculated_find_formula_errors($formula)) {
792 return $error;
793 }
794 /// Calculate the correct answer
795 if (empty($formula)) {
796 $str = '';
797 } else if ($formula === '*'){
798 $str = '*';
799 } else {
800 eval('$str = '.$formula.';');
801 }
802 return $str;
803 }
804
805 function get_dataset_definitions($questionid, $newdatasets) {
806 global $DB;
807 //get the existing datasets for this question
808 $datasetdefs = array();
809 if (!empty($questionid)) {
810 global $CFG;
811 $sql = "SELECT i.*
812 FROM {question_datasets} d,
813 {question_dataset_definitions} i
814 WHERE d.question = ?
815 AND d.datasetdefinition = i.id
816 ";
817 if ($records = $DB->get_records_sql($sql, array($questionid))) {
818 foreach ($records as $r) {
819 $datasetdefs["$r->type-$r->category-$r->name"] = $r;
820 }
821 }
822 }
823
824 foreach ($newdatasets as $dataset) {
825 if (!$dataset) {
826 continue; // The no dataset case...
827 }
828
829 if (!isset($datasetdefs[$dataset])) {
830 //make new datasetdef
831 list($type, $category, $name) = explode('-', $dataset, 3);
832 $datasetdef = new stdClass;
833 $datasetdef->type = $type;
834 $datasetdef->name = $name;
835 $datasetdef->category = $category;
836 $datasetdef->itemcount = 0;
837 $datasetdef->options = 'uniform:1.0:10.0:1';
838 $datasetdefs[$dataset] = clone($datasetdef);
839 }
840 }
841 return $datasetdefs;
842 }
843
844 function save_dataset_definitions($form) {
845 global $DB;
846 // save synchronize
847
848 // Save datasets
849 $datasetdefinitions = $this->get_dataset_definitions($form->id, $form->dataset);
850 $tmpdatasets = array_flip($form->dataset);
851 $defids = array_keys($datasetdefinitions);
852 foreach ($defids as $defid) {
853 $datasetdef = &$datasetdefinitions[$defid];
854 if (isset($datasetdef->id)) {
855 if (!isset($tmpdatasets[$defid])) {
856 // This dataset is not used any more, delete it
857 $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $datasetdef->id));
858 if ($datasetdef->category == 0) { // Question local dataset
859 $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id));
860 $DB->delete_records('question_dataset_items', array('definition' => $datasetdef->id));
861 }
862 }
863 // This has already been saved or just got deleted
864 unset($datasetdefinitions[$defid]);
865 continue;
866 }
867
868 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
869
870 if (0 != $datasetdef->category) {
871 // We need to look for already existing
872 // datasets in the category.
873 // By first creating the datasetdefinition above we
874 // can manage to automatically take care of
875 // some possible realtime concurrence
876 if ($olderdatasetdefs = $DB->get_records_select( 'question_dataset_definitions',
877 "type = ?
878 AND name = ?
879 AND category = ?
880 AND id < ?
881 ORDER BY id DESC", array($datasetdef->type, $datasetdef->name, $datasetdef->category, $datasetdef->id))) {
882
883 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
884 $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id));
885 $datasetdef = $olderdatasetdef;
886 }
887 }
888 }
889
890 // Create relation to this dataset:
891 $questiondataset = new stdClass;
892 $questiondataset->question = $form->id;
893 $questiondataset->datasetdefinition = $datasetdef->id;
894 $DB->insert_record('question_datasets', $questiondataset);
895 unset($datasetdefinitions[$defid]);
896 }
897
898 // Remove local obsolete datasets as well as relations
899 // to datasets in other categories:
900 if (!empty($datasetdefinitions)) {
901 foreach ($datasetdefinitions as $def) {
902 $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $def->id));
903
904 if ($def->category == 0) { // Question local dataset
905 $DB->delete_records('question_dataset_definitions', array('id' => $def->id));
906 $DB->delete_records('question_dataset_items', array('definition' => $def->id));
907 }
908 }
909 }
910 }
911 /** This function create a copy of the datasets ( definition and dataitems)
912 * from the preceding question if they remain in the new question
913 * otherwise its create the datasets that have been added as in the
914 * save_dataset_definitions()
915 */
916 function save_as_new_dataset_definitions($form, $initialid) {
917 global $CFG, $DB;
918 // Get the datasets from the intial question
919 $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset);
920 // $tmpdatasets contains those of the new question
921 $tmpdatasets = array_flip($form->dataset);
922 $defids = array_keys($datasetdefinitions);// new datasets
923 foreach ($defids as $defid) {
924 $datasetdef = &$datasetdefinitions[$defid];
925 if (isset($datasetdef->id)) {
926 // This dataset exist in the initial question
927 if (!isset($tmpdatasets[$defid])) {
928 // do not exist in the new question so ignore
929 unset($datasetdefinitions[$defid]);
930 continue;
931 }
932 // create a copy but not for category one
933 if (0 == $datasetdef->category) {
934 $olddatasetid = $datasetdef->id ;
935 $olditemcount = $datasetdef->itemcount ;
936 $datasetdef->itemcount =0;
937 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
938 //copy the dataitems
939 $olditems = $this->get_database_dataset_items($olddatasetid);
940 if (count($olditems) > 0 ) {
941 $itemcount = 0;
942 foreach($olditems as $item ){
943 $item->definition = $datasetdef->id;
944 $DB->insert_record('question_dataset_items', $item);
945 $itemcount++;
946 }
947 //update item count
948 $datasetdef->itemcount =$itemcount;
949 $DB->update_record('question_dataset_definitions', $datasetdef);
950 } // end of copy the dataitems
951 }// end of copy the datasetdef
952 // Create relation to the new question with this
953 // copy as new datasetdef from the initial question
954 $questiondataset = new stdClass;
955 $questiondataset->question = $form->id;
956 $questiondataset->datasetdefinition = $datasetdef->id;
957 $DB->insert_record('question_datasets', $questiondataset);
958 unset($datasetdefinitions[$defid]);
959 continue;
960 }// end of datasetdefs from the initial question
961 // really new one code similar to save_dataset_definitions()
962 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
963
964 if (0 != $datasetdef->category) {
965 // We need to look for already existing
966 // datasets in the category.
967 // By first creating the datasetdefinition above we
968 // can manage to automatically take care of
969 // some possible realtime concurrence
970 if ($olderdatasetdefs = $DB->get_records_select(
971 'question_dataset_definitions',
972 "type = ?
973 AND name = ?
974 AND category = ?
975 AND id < ?
976 ORDER BY id DESC", array($datasetdef->type, $datasetdef->name, $datasetdef->category, $datasetdef->id))) {
977
978 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
979 $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id));
980 $datasetdef = $olderdatasetdef;
981 }
982 }
983 }
984
985 // Create relation to this dataset:
986 $questiondataset = new stdClass;
987 $questiondataset->question = $form->id;
988 $questiondataset->datasetdefinition = $datasetdef->id;
989 $DB->insert_record('question_datasets', $questiondataset);
990 unset($datasetdefinitions[$defid]);
991 }
992
993 // Remove local obsolete datasets as well as relations
994 // to datasets in other categories:
995 if (!empty($datasetdefinitions)) {
996 foreach ($datasetdefinitions as $def) {
997 $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $def->id));
998
999 if ($def->category == 0) { // Question local dataset
1000 $DB->delete_records('question_dataset_definitions', array('id' => $def->id));
1001 $DB->delete_records('question_dataset_items', array('definition' => $def->id));
1002 }
1003 }
1004 }
1005 }
1006
1007/// Dataset functionality
1008 function pick_question_dataset($question, $datasetitem) {
1009 // Select a dataset in the following format:
1010 // An array indexed by the variable names (d.name) pointing to the value
1011 // to be substituted
1012 global $CFG, $DB;
1013 if (!$dataitems = $DB->get_records_sql(
1014 "SELECT i.id, d.name, i.value
1015 FROM {question_dataset_definitions} d,
1016 {question_dataset_items} i,
1017 {question_datasets} q
1018 WHERE q.question = ?
1019 AND q.datasetdefinition = d.id
1020 AND d.id = i.definition
1021 AND i.itemnumber = ? ORDER by i.id DESC ", array($question->id, $datasetitem))) {
1022 print_error('cannotgetdsfordependent', 'question', '', array($question->id, $datasetitem));
1023 }
1024 $dataset = Array();
1025 foreach($dataitems as $id => $dataitem ){
1026 if (!isset($dataset[$dataitem->name])){
1027 $dataset[$dataitem->name] = $dataitem->value ;
1028 }else {
1029 // deleting the unused records could be added here
1030 }
1031 }
1032 return $dataset;
1033 }
1034
1035 function dataset_options_from_database($form, $name,$prefix='',$langfile='quiz') {
1036
1037 // First options - it is not a dataset...
1038 $options['0'] = get_string($prefix.'nodataset', $langfile);
1039
1040 // Construct question local options
1041 global $CFG, $DB;
1042 $type = 1 ; // only type = 1 (i.e. old 'LITERAL') has ever been used
1043 if ( ! $currentdatasetdef = $DB->get_record_sql(
1044 "SELECT a.*
1045 FROM {question_dataset_definitions} a,
1046 {question_datasets} b
1047 WHERE a.id = b.datasetdefinition
1048 AND a.type = '1'
1049 AND b.question = ?
1050 AND a.name = ?", array($form->id, $name))){
1051 $currentdatasetdef->type = '0';
1052 };
1053 $key = "$type-0-$name";
1054 if ($currentdatasetdef->type == $type
1055 and $currentdatasetdef->category == 0) {
1056 $options[$key] = get_string($prefix."keptlocal$type", $langfile);
1057 } else {
1058 $options[$key] = get_string($prefix."newlocal$type", $langfile);
1059 }
1060 // Construct question category options
1061 $categorydatasetdefs = $DB->get_records_sql(
1062 "SELECT b.question, a.*
1063 FROM {question_datasets} b,
1064 {question_dataset_definitions} a
1065 WHERE a.id = b.datasetdefinition
1066 AND a.type = '1'
1067 AND a.category = ?
1068 AND a.name = ?", array($form->category, $name));
1069 $type = 1 ;
1070 $key = "$type-$form->category-$name";
1071 if (!empty($categorydatasetdefs)){ // there is at least one with the same name
1072 if (isset($categorydatasetdefs[$form->id])) {// it is already used by this question
1073 $options[$key] = get_string($prefix."keptcategory$type", $langfile);
1074 } else {
1075 $options[$key] = get_string($prefix."existingcategory$type", $langfile);
1076 }
1077 } else {
1078 $options[$key] = get_string($prefix."newcategory$type", $langfile);
1079 }
1080 // All done!
1081 return array($options, $currentdatasetdef->type
1082 ? "$currentdatasetdef->type-$currentdatasetdef->category-$name"
1083 : '');
1084 }
1085
1086 function find_dataset_names($text) {
1087 /// Returns the possible dataset names found in the text as an array
1088 /// The array has the dataset name for both key and value
1089 $datasetnames = array();
1090 while (preg_match('~\\{([[:alpha:]][^>} <{"\']*)\\}~', $text, $regs)) {
1091 $datasetnames[$regs[1]] = $regs[1];
1092 $text = str_replace($regs[0], '', $text);
1093 }
1094 return $datasetnames;
1095 }
1096
1097 /**
1098 * This function retrieve the item count of the available category shareable
1099 * wild cards that is added as a comment displayed when a wild card with
1100 * the same name is displayed in datasetdefinitions_form.php
1101 */
1102 function get_dataset_definitions_category($form) {
1103 global $CFG, $DB;
1104 $datasetdefs = array();
1105 $lnamemax = 30;
1106 if (!empty($form->category)) {
1107 $sql = "SELECT i.*,d.*
1108 FROM {question_datasets} d,
1109 {question_dataset_definitions} i
1110 WHERE i.id = d.datasetdefinition
1111 AND i.category = ?
1112 ;
1113 ";
1114 if ($records = $DB->get_records_sql($sql, array($form->category))) {
1115 foreach ($records as $r) {
1116 if ( !isset ($datasetdefs["$r->name"])) $datasetdefs["$r->name"] = $r->itemcount;
1117 }
1118 }
1119 }
1120 return $datasetdefs ;
1121 }
1122
1123 /**
1124 * This function build a table showing the available category shareable
1125 * wild cards, their name, their definition (Min, Max, Decimal) , the item count
1126 * and the name of the question where they are used.
1127 * This table is intended to be add before the question text to help the user use
1128 * these wild cards
1129 */
1130
1131 function print_dataset_definitions_category($form) {
1132 global $CFG, $DB;
1133 $datasetdefs = array();
1134 $lnamemax = 22;
1135 $namestr =get_string('name', 'quiz');
1136 $minstr=get_string('min', 'quiz');
1137 $maxstr=get_string('max', 'quiz');
1138 $rangeofvaluestr=get_string('minmax','qtype_datasetdependent');
1139 $questionusingstr = get_string('usedinquestion','qtype_calculated');
1140 $itemscountstr = get_string('itemscount','qtype_datasetdependent');
1141 $text ='';
1142 if (!empty($form->category)) {
1143 list($category) = explode(',', $form->category);
1144 $sql = "SELECT i.*,d.*
1145 FROM {question_datasets} d,
1146 {question_dataset_definitions} i
1147 WHERE i.id = d.datasetdefinition
1148 AND i.category = ?;
1149 " ;
1150 if ($records = $DB->get_records_sql($sql, array($category))) {
1151 foreach ($records as $r) {
1152 $sql1 = "SELECT q.*
1153 FROM {question} q
1154 WHERE q.id = ?
1155 ";
1156 if ( !isset ($datasetdefs["$r->type-$r->category-$r->name"])){
1157 $datasetdefs["$r->type-$r->category-$r->name"]= $r;
1158 }
1159 if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
1160 $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question]->name =$questionb[$r->question]->name ;
1161 }
1162 }
1163 }
1164 }
1165 if (!empty ($datasetdefs)){
1166
1167 $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>";
1168 foreach ($datasetdefs as $datasetdef){
1169 list($distribution, $min, $max,$dec) = explode(':', $datasetdef->options, 4);
1170 $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\">";
1171 foreach ($datasetdef->questions as $qu) {
1172 //limit the name length displayed
1173 if (!empty($qu->name)) {
1174 $qu->name = (strlen($qu->name) > $lnamemax) ?
1175 substr($qu->name, 0, $lnamemax).'...' : $qu->name;
1176 } else {
1177 $qu->name = '';
1178 }
1179 $text .=" &nbsp;&nbsp; $qu->name <br/>";
1180 }
1181 $text .="</td></tr>";
1182 }
1183 $text .="</table>";
1184 }else{
1185 $text .=get_string('nosharedwildcard', 'qtype_calculated');
1186 }
1187 return $text ;
1188 }
1189 function get_virtual_qtype() {
1190 global $QTYPES;
1191 // if ( isset($question->options->multichoice) && $question->options->multichoice == '1'){
1192 $this->virtualqtype =& $QTYPES['multichoice'];
1193 // }else {
1194 // $this->virtualqtype =& $QTYPES['numerical'];
1195 // }
1196 return $this->virtualqtype;
1197 }
1198
1199
1200/// BACKUP FUNCTIONS ////////////////////////////
1201
1202 /*
1203 * Backup the data in the question
1204 *
1205 * This is used in question/backuplib.php
1206 */
1207 function backup($bf,$preferences,$question,$level=6) {
1208 global $DB;
1209 $status = true;
1210
1211 $calculateds = $DB->get_records("question_calculated",array("question" =>$question,"id"));
1212 //If there are calculated-s
1213 if ($calculateds) {
1214 //Iterate over each calculateds
1215 foreach ($calculateds as $calculated) {
1216 $status = $status &&fwrite ($bf,start_tag("CALCULATED",$level,true));
1217 //Print calculated contents
1218 fwrite ($bf,full_tag("ANSWER",$level+1,false,$calculated->answer));
1219 fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$calculated->tolerance));
1220 fwrite ($bf,full_tag("TOLERANCETYPE",$level+1,false,$calculated->tolerancetype));
1221 fwrite ($bf,full_tag("CORRECTANSWERLENGTH",$level+1,false,$calculated->correctanswerlength));
1222 fwrite ($bf,full_tag("CORRECTANSWERFORMAT",$level+1,false,$calculated->correctanswerformat));
1223 //Now backup numerical_units
1224 $status = question_backup_numerical_units($bf,$preferences,$question,7);
1225 //Now backup required dataset definitions and items...
1226 $status = question_backup_datasets($bf,$preferences,$question,7);
1227 //End calculated data
1228 $status = $status &&fwrite ($bf,end_tag("CALCULATED",$level,true));
1229 }
1230 $calculated_options = $DB->get_records("question_calculated_options",array("questionid" => $question),"id");
1231 if ($calculated_options) {
1232 //Iterate over each calculated_option
1233 foreach ($calculated_options as $calculated_option) {
1234 $status = fwrite ($bf,start_tag("CALCULATED_OPTIONS",$level,true));
1235 //Print calculated_option contents
1236 fwrite ($bf,full_tag("SYNCHRONIZE",$level+1,false,$calculated_option->synchronize));
1237 fwrite ($bf,full_tag("MULTIPLECHOICE",$level+1,false,$calculated_option->multiplechoice));
1238 fwrite ($bf,full_tag("SINGLE",$level+1,false,$calculated_option->single));
1239 fwrite ($bf,full_tag("SHUFFLEANSWERS",$level+1,false,$calculated_option->shuffleanswers));
1240 fwrite ($bf,full_tag("CORRECTFEEDBACK",$level+1,false,$calculated_option->correctfeedback));
1241 fwrite ($bf,full_tag("PARTIALLYCORRECTFEEDBACK",$level+1,false,$calculated_option->partiallycorrectfeedback));
1242 fwrite ($bf,full_tag("INCORRECTFEEDBACK",$level+1,false,$calculated_option->incorrectfeedback));
1243 fwrite ($bf,full_tag("ANSWERNUMBERING",$level+1,false,$calculated_option->answernumbering));
1244 $status = fwrite ($bf,end_tag("CALCULATED_OPTIONS",$level,true));
1245 }
1246 }
1247 $status = question_backup_numerical_options($bf,$preferences,$question,$level);
1248
1249 }
1250 return $status;
1251 }
1252
1253/// RESTORE FUNCTIONS /////////////////
1254
1255 /*
1256 * Restores the data in the question
1257 *
1258 * This is used in question/restorelib.php
1259 */
1260 function restore($old_question_id,$new_question_id,$info,$restore) {
1261 global $DB;
1262
1263 $status = true;
1264
1265 //Get the calculated-s array
1266 $calculateds = $info['#']['CALCULATED'];
1267
1268 //Iterate over calculateds
1269 for($i = 0; $i < sizeof($calculateds); $i++) {
1270 $cal_info = $calculateds[$i];
1271 //traverse_xmlize($cal_info); //Debug
1272 //print_object ($GLOBALS['traverse_array']); //Debug
1273 //$GLOBALS['traverse_array']=""; //Debug
1274
1275 //Now, build the question_calculated record structure
1276 $calculated->question = $new_question_id;
1277 $calculated->answer = backup_todb($cal_info['#']['ANSWER']['0']['#']);
1278 $calculated->tolerance = backup_todb($cal_info['#']['TOLERANCE']['0']['#']);
1279 $calculated->tolerancetype = backup_todb($cal_info['#']['TOLERANCETYPE']['0']['#']);
1280 $calculated->correctanswerlength = backup_todb($cal_info['#']['CORRECTANSWERLENGTH']['0']['#']);
1281 $calculated->correctanswerformat = backup_todb($cal_info['#']['CORRECTANSWERFORMAT']['0']['#']);
1282
1283 ////We have to recode the answer field
1284 $answer = backup_getid($restore->backup_unique_code,"question_answers",$calculated->answer);
1285 if ($answer) {
1286 $calculated->answer = $answer->new_id;
1287 }
1288
1289 //The structure is equal to the db, so insert the question_calculated
1290 $newid = $DB->insert_record ("question_calculated",$calculated);
1291
1292 //Do some output
1293 if (($i+1) % 50 == 0) {
1294 if (!defined('RESTORE_SILENTLY')) {
1295 echo ".";
1296 if (($i+1) % 1000 == 0) {
1297 echo "<br />";
1298 }
1299 }
1300 backup_flush(300);
1301 }
1302 //Get the calculated_options array
1303 // need to check as old questions don't have calculated_options record
1304 if(isset($info['#']['CALCULATED_OPTIONS'])){
1305 $calculatedoptions = $info['#']['CALCULATED_OPTIONS'];
1306
1307 //Iterate over calculated_options
1308 for($i = 0; $i < sizeof($calculatedoptions); $i++){
1309 $cal_info = $calculatedoptions[$i];
1310 //traverse_xmlize($cal_info); //Debug
1311 //print_object ($GLOBALS['traverse_array']); //Debug
1312 //$GLOBALS['traverse_array']=""; //Debug
1313
1314 //Now, build the question_calculated_options record structure
1315 $calculated_options->questionid = $new_question_id;
1316 $calculated_options->synchronize = backup_todb($cal_info['#']['SYNCHRONIZE']['0']['#']);
1317 // $calculated_options->multichoice = backup_todb($cal_info['#']['MULTICHOICE']['0']['#']);
1318 $calculated_options->single = backup_todb($cal_info['#']['SINGLE']['0']['#']);
1319 $calculated_options->shuffleanswers = isset($cal_info['#']['SHUFFLEANSWERS']['0']['#'])?backup_todb($mul_info['#']['SHUFFLEANSWERS']['0']['#']):'';
1320 $calculated_options->correctfeedback = backup_todb($cal_info['#']['CORRECTFEEDBACK']['0']['#']);
1321 $calculated_options->partiallycorrectfeedback = backup_todb($cal_info['#']['PARTIALLYCORRECTFEEDBACK']['0']['#']);
1322 $calculated_options->incorrectfeedback = backup_todb($cal_info['#']['INCORRECTFEEDBACK']['0']['#']);
1323 $calculated_options->answernumbering = backup_todb($cal_info['#']['ANSWERNUMBERING']['0']['#']);
1324
1325 //The structure is equal to the db, so insert the question_calculated_options
1326 $newid = $DB->insert_record ("question_calculated_options",$calculated_options);
1327
1328 //Do some output
1329 if (($i+1) % 50 == 0) {
1330 if (!defined('RESTORE_SILENTLY')) {
1331 echo ".";
1332 if (($i+1) % 1000 == 0) {
1333 echo "<br />";
1334 }
1335 }
1336 backup_flush(300);
1337 }
1338 }
1339 }
1340 //Now restore numerical_units
1341 $status = question_restore_numerical_units ($old_question_id,$new_question_id,$cal_info,$restore);
1342 $status = question_restore_numerical_options($old_question_id,$new_question_id,$info,$restore);
1343 //Now restore dataset_definitions
1344 if ($status && $newid) {
1345 $status = question_restore_dataset_definitions ($old_question_id,$new_question_id,$cal_info,$restore);
1346 }
1347
1348 if (!$newid) {
1349 $status = false;
1350 }
1351 }
1352
1353 return $status;
1354 }
1355
1356 /**
1357 * Runs all the code required to set up and save an essay question for testing purposes.
1358 * Alternate DB table prefix may be used to facilitate data deletion.
1359 */
1360 function generate_test($name, $courseid = null) {
1361 global $DB;
1362 list($form, $question) = parent::generate_test($name, $courseid);
1363 $form->feedback = 1;
1364 $form->multiplier = array(1, 1);
1365 $form->shuffleanswers = 1;
1366 $form->noanswers = 1;
1367 $form->qtype ='calculatedmulti';
1368 $question->qtype ='calculatedmulti';
1369 $form->answers = array('{a} + {b}');
1370 $form->fraction = array(1);
1371 $form->tolerance = array(0.01);
1372 $form->tolerancetype = array(1);
1373 $form->correctanswerlength = array(2);
1374 $form->correctanswerformat = array(1);
1375 $form->questiontext = "What is {a} + {b}?";
1376
1377 if ($courseid) {
1378 $course = $DB->get_record('course', array('id'=> $courseid));
1379 }
1380
1381 $new_question = $this->save_question($question, $form, $course);
1382
1383 $dataset_form = new stdClass();
1384 $dataset_form->nextpageparam["forceregeneration"]= 1;
1385 $dataset_form->calcmin = array(1 => 1.0, 2 => 1.0);
1386 $dataset_form->calcmax = array(1 => 10.0, 2 => 10.0);
1387 $dataset_form->calclength = array(1 => 1, 2 => 1);
1388 $dataset_form->number = array(1 => 5.4 , 2 => 4.9);
1389 $dataset_form->itemid = array(1 => '' , 2 => '');
1390 $dataset_form->calcdistribution = array(1 => 'uniform', 2 => 'uniform');
1391 $dataset_form->definition = array(1 => "1-0-a",
1392 2 => "1-0-b");
1393 $dataset_form->nextpageparam = array('forceregeneration' => false);
1394 $dataset_form->addbutton = 1;
1395 $dataset_form->selectadd = 1;
1396 $dataset_form->courseid = $courseid;
1397 $dataset_form->cmid = 0;
1398 $dataset_form->id = $new_question->id;
1399 $this->save_dataset_items($new_question, $dataset_form);
1400
1401 return $new_question;
1402 }
1403}
1404//// END OF CLASS ////
1405
1406//////////////////////////////////////////////////////////////////////////
1407//// INITIATION - Without this line the question type is not in use... ///
1408//////////////////////////////////////////////////////////////////////////
1409question_register_questiontype(new question_calculatedmulti_qtype());
1410
1411function qtype_calculatedmulti_calculate_answer($formula, $individualdata,
1412 $tolerance, $tolerancetype, $answerlength, $answerformat='1', $unit='') {
1413/// The return value has these properties:
1414/// ->answer the correct answer
1415/// ->min the lower bound for an acceptable response
1416/// ->max the upper bound for an accetpable response
1417
1418 /// Exchange formula variables with the correct values...
1419 global $QTYPES;
1420 $answer = $QTYPES['calculated']->substitute_variables_and_eval($formula, $individualdata);
1421 if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */
1422 /*** Adjust to the correct number of decimals ***/
1423 if (stripos($answer,'e')>0 ){
1424 $answerlengthadd = strlen($answer)-stripos($answer,'e');
1425 }else {
1426 $answerlengthadd = 0 ;
1427 }
1428 $calculated->answer = round(floatval($answer), $answerlength+$answerlengthadd);
1429
1430 if ($answerlength) {
1431 /* Try to include missing zeros at the end */
1432
1433 if (preg_match('~^(.*\\.)(.*)$~', $calculated->answer, $regs)) {
1434 $calculated->answer = $regs[1] . substr(
1435 $regs[2] . '00000000000000000000000000000000000000000x',
1436 0, $answerlength)
1437 . $unit;
1438 } else {
1439 $calculated->answer .=
1440 substr('.00000000000000000000000000000000000000000x',
1441 0, $answerlength + 1) . $unit;
1442 }
1443 } else {
1444 /* Attach unit */
1445 $calculated->answer .= $unit;
1446 }
1447
1448 } else if ($answer) { // Significant figures does only apply if the result is non-zero
1449
1450 // Convert to positive answer...
1451 if ($answer < 0) {
1452 $answer = -$answer;
1453 $sign = '-';
1454 } else {
1455 $sign = '';
1456 }
1457
1458 // Determine the format 0.[1-9][0-9]* for the answer...
1459 $p10 = 0;
1460 while ($answer < 1) {
1461 --$p10;
1462 $answer *= 10;
1463 }
1464 while ($answer >= 1) {
1465 ++$p10;
1466 $answer /= 10;
1467 }
1468 // ... and have the answer rounded of to the correct length
1469 $answer = round($answer, $answerlength);
1470
1471 // Have the answer written on a suitable format,
1472 // Either scientific or plain numeric
1473 if (-2 > $p10 || 4 < $p10) {
1474 // Use scientific format:
1475 $eX = 'e'.--$p10;
1476 $answer *= 10;
1477 if (1 == $answerlength) {
1478 $calculated->answer = $sign.$answer.$eX.$unit;
1479 } else {
1480 // Attach additional zeros at the end of $answer,
1481 $answer .= (1==strlen($answer) ? '.' : '')
1482 . '00000000000000000000000000000000000000000x';
1483 $calculated->answer = $sign
1484 .substr($answer, 0, $answerlength +1).$eX.$unit;
1485 }
1486 } else {
1487 // Stick to plain numeric format
1488 $answer *= "1e$p10";
1489 if (0.1 <= $answer / "1e$answerlength") {
1490 $calculated->answer = $sign.$answer.$unit;
1491 } else {
1492 // Could be an idea to add some zeros here
1493 $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '')
1494 . '00000000000000000000000000000000000000000x';
1495 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
1496 $calculated->answer = $sign.substr($answer, 0, $oklen).$unit;
1497 }
1498 }
1499
1500 } else {
1501 $calculated->answer = 0.0;
1502 }
1503
1504 /// Return the result
1505 return $calculated;
1506}
1507
1508
1509function qtype_calculatedmulti_find_formula_errors($formula) {
1510/// Validates the formula submitted from the question edit page.
1511/// Returns false if everything is alright.
1512/// Otherwise it constructs an error message
1513 // Strip away dataset names
1514 while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) {
1515 $formula = str_replace($regs[0], '1', $formula);
1516 }
1517
1518 // Strip away empty space and lowercase it
1519 $formula = strtolower(str_replace(' ', '', $formula));
1520
1521 $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */
1522 $operatorornumber = "[$safeoperatorchar.0-9eE]";
1523
1524 while ( preg_match("~(^|[$safeoperatorchar,(])([a-z0-9_]*)\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)~",
1525 $formula, $regs)) {
1526
1527 switch ($regs[2]) {
1528 // Simple parenthesis
1529 case '':
1530 if ($regs[4] || strlen($regs[3])==0) {
1531 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
1532 }
1533 break;
1534
1535 // Zero argument functions
1536 case 'pi':
1537 if ($regs[3]) {
1538 return get_string('functiontakesnoargs', 'quiz', $regs[2]);
1539 }
1540 break;
1541
1542 // Single argument functions (the most common case)
1543 case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
1544 case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
1545 case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
1546 case 'exp': case 'expm1': case 'floor': case 'is_finite':
1547 case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
1548 case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
1549 case 'tan': case 'tanh':
1550 if (!empty($regs[4]) || empty($regs[3])) {
1551 return get_string('functiontakesonearg','quiz',$regs[2]);
1552 }
1553 break;
1554
1555 // Functions that take one or two arguments
1556 case 'log': case 'round':
1557 if (!empty($regs[5]) || empty($regs[3])) {
1558 return get_string('functiontakesoneortwoargs','quiz',$regs[2]);
1559 }
1560 break;
1561
1562 // Functions that must have two arguments
1563 case 'atan2': case 'fmod': case 'pow':
1564 if (!empty($regs[5]) || empty($regs[4])) {
1565 return get_string('functiontakestwoargs', 'quiz', $regs[2]);
1566 }
1567 break;
1568
1569 // Functions that take two or more arguments
1570 case 'min': case 'max':
1571 if (empty($regs[4])) {
1572 return get_string('functiontakesatleasttwo','quiz',$regs[2]);
1573 }
1574 break;
1575
1576 default:
1577 return get_string('unsupportedformulafunction','quiz',$regs[2]);
1578 }
1579
1580 // Exchange the function call with '1' and then chack for
1581 // another function call...
1582 if ($regs[1]) {
1583 // The function call is proceeded by an operator
1584 $formula = str_replace($regs[0], $regs[1] . '1', $formula);
1585 } else {
1586 // The function call starts the formula
1587 $formula = preg_replace("~^$regs[2]\\([^)]*\\)~", '1', $formula);
1588 }
1589 }
1590
1591 if (preg_match("~[^$safeoperatorchar.0-9eE]+~", $formula, $regs)) {
1592 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
1593 } else {
1594 // Formula just might be valid
1595 return false;
1596 }
1597
1598}
1599
1600
1601