MDL-19057 No more exceptions in tests for oracle. If tests fail, MDL-20339 can be...
[moodle.git] / question / type / calculated / questiontype.php
CommitLineData
516cf3eb 1<?php // $Id$
3f76dd52 2
516cf3eb 3/////////////////
a2156789 4// CALCULATED ///
516cf3eb 5/////////////////
6
7/// QUESTION TYPE CLASS //////////////////
8
516cf3eb 9
fd6b864f 10
fbe2cfea 11class question_calculated_qtype extends default_questiontype {
516cf3eb 12
13 // Used by the function custom_generator_tools:
14 var $calcgenerateidhasbeenadded = false;
fbe2cfea 15 public $virtualqtype = false;
516cf3eb 16
17 function name() {
18 return 'calculated';
19 }
20
869309b8 21 function has_wildcards_in_responses($question, $subqid) {
22 return true;
23 }
24
25 function requires_qtypes() {
26 return array('numerical');
27 }
28
516cf3eb 29 function get_question_options(&$question) {
30 // First get the datasets and default options
fef8f84e 31 global $CFG, $DB, $OUTPUT;
f34488b2 32 if (!$question->options->answers = $DB->get_records_sql(
a6d46515 33 "SELECT a.*, c.tolerance, c.tolerancetype, c.correctanswerlength, c.correctanswerformat " .
f34488b2 34 "FROM {question_answers} a, " .
35 " {question_calculated} c " .
36 "WHERE a.question = ? " .
a6d46515 37 "AND a.id = c.answer ".
f34488b2 38 "ORDER BY a.id ASC", array($question->id))) {
fef8f84e 39 echo $OUTPUT->notification('Error: Missing question answer for calculated question ' . $question->id . '!');
516cf3eb 40 return false;
41 }
1ee53ca9 42 if (!$question->options->synchronize = $DB->get_field('question_calculated_options', 'synchronize', array('question' => $question->id)) ) {
43 $question->options->synchronize = 0;
44 }
516cf3eb 45
a6d46515 46/*
47 if(false === parent::get_question_options($question)) {
48 return false;
49 }
50
f34488b2 51 if (!$options = $DB->get_records('question_calculated', array('question' => $question->id))) {
516cf3eb 52 notify("No options were found for calculated question
53 #{$question->id}! Proceeding with defaults.");
f34488b2 54 // $options = new Array();
a6d46515 55 $options= new stdClass;
516cf3eb 56 $options->tolerance = 0.01;
57 $options->tolerancetype = 1; // relative
58 $options->correctanswerlength = 2;
59 $options->correctanswerformat = 1; // decimals
f34488b2 60 }
516cf3eb 61
62 // For historic reasons we also need these fields in the answer objects.
63 // This should eventually be removed and related code changed to use
64 // the values in $question->options instead.
a6d46515 65 foreach ($question->options->answers as $key => $answer) {
516cf3eb 66 $answer = &$question->options->answers[$key]; // for PHP 4.x
a6d46515 67 $answer->calcid = $options->id;
516cf3eb 68 $answer->tolerance = $options->tolerance;
69 $answer->tolerancetype = $options->tolerancetype;
70 $answer->correctanswerlength = $options->correctanswerlength;
71 $answer->correctanswerformat = $options->correctanswerformat;
a6d46515 72 }*/
516cf3eb 73
74 $virtualqtype = $this->get_virtual_qtype();
75 $virtualqtype->get_numerical_units($question);
f34488b2 76
c9e4ba36 77 if( isset($question->export_process)&&$question->export_process){
78 $question->options->datasets = $this->get_datasets_for_export($question);
f34488b2 79 }
516cf3eb 80 return true;
81 }
f34488b2 82
c9e4ba36 83 function get_datasets_for_export(&$question){
f34488b2 84 global $DB;
3f76dd52 85 $datasetdefs = array();
c9e4ba36 86 if (!empty($question->id)) {
3f76dd52 87 global $CFG;
88 $sql = "SELECT i.*
f34488b2 89 FROM {question_datasets} d,
90 {question_dataset_definitions} i
91 WHERE d.question = ?
3f76dd52 92 AND d.datasetdefinition = i.id
93 ";
f34488b2 94 if ($records = $DB->get_records_sql($sql, array($question->id))) {
3f76dd52 95 foreach ($records as $r) {
96 $def = $r ;
97 if ($def->category=='0'){
98 $def->status='private';
f34488b2 99 } else {
3f76dd52 100 $def->status='shared';
f34488b2 101 }
3f76dd52 102 $def->type ='calculated' ;
103 list($distribution, $min, $max,$dec) = explode(':', $def->options, 4);
104 $def->distribution=$distribution;
105 $def->minimum=$min;
106 $def->maximum=$max;
f34488b2 107 $def->decimals=$dec ;
3f76dd52 108 if ($def->itemcount > 0 ) {
109 // get the datasetitems
110 $def->items = array();
a2155a7b 111 if ($items = $this->get_database_dataset_items($def->id)){
3f76dd52 112 $n = 0;
113 foreach( $items as $ii){
114 $n++;
115 $def->items[$n] = new stdClass;
116 $def->items[$n]->itemnumber=$ii->itemnumber;
117 $def->items[$n]->value=$ii->value;
118 }
119 $def->number_of_items=$n ;
120 }
121 }
f34488b2 122 $datasetdefs["1-$r->category-$r->name"] = $def;
3f76dd52 123 }
124 }
125 }
126 return $datasetdefs ;
f34488b2 127 }
128
516cf3eb 129 function save_question_options($question) {
130 //$options = $question->subtypeoptions;
131 // Get old answers:
f34488b2 132 global $CFG, $DB;
f7fa6874 133 if (isset($question->answer) && !isset($question->answers)) {
2aef1fe5 134 $question->answers = $question->answer;
135 }
1ee53ca9 136 // calculated options
137 $update = true ;
138 $options = $DB->get_record("question_calculated_options", array("question" => $question->id));
139 if (!$options) {
140 $update = false;
141 $options = new stdClass;
142 $options->question = $question->id;
143 }
144 $options->synchronize = $question->synchronize;
145 if ($update) {
146 if (!$DB->update_record("question_calculated_options", $options)) {
147 $result->error = "Could not update calculated question options! (id=$options->id)";
148 return $result;
149 }
150 } else {
151 if (!$DB->insert_record("question_calculated_options", $options)) {
152 $result->error = "Could not insert calculated question options!";
153 return $result;
154 }
155 }
2aef1fe5 156
a6d46515 157 // Get old versions of the objects
f34488b2 158 if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) {
516cf3eb 159 $oldanswers = array();
160 }
161
f34488b2 162 if (!$oldoptions = $DB->get_records('question_calculated', array('question' => $question->id), 'answer ASC')) {
a6d46515 163 $oldoptions = array();
164 }
2aef1fe5 165
166 // Save the units.
a6d46515 167 $virtualqtype = $this->get_virtual_qtype();
168 $result = $virtualqtype->save_numerical_units($question);
169 if (isset($result->error)) {
170 return $result;
171 } else {
172 $units = &$result->units;
173 }
174 // Insert all the new answers
0a6555f8 175 if (isset($question->answer) && !isset($question->answers)) {
176 $question->answers=$question->answer;
177 }
a6d46515 178 foreach ($question->answers as $key => $dataanswer) {
f34488b2 179 if ( trim($dataanswer) != '' ) {
a6d46515 180 $answer = new stdClass;
181 $answer->question = $question->id;
182 $answer->answer = trim($dataanswer);
183 $answer->fraction = $question->fraction[$key];
184 $answer->feedback = trim($question->feedback[$key]);
f34488b2 185
a6d46515 186 if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it
187 $answer->id = $oldanswer->id;
0bcf8b6f 188 $DB->update_record("question_answers", $answer);
a6d46515 189 } else { // This is a completely new answer
0bcf8b6f 190 $answer->id = $DB->insert_record("question_answers", $answer);
516cf3eb 191 }
a6d46515 192
193 // Set up the options object
194 if (!$options = array_shift($oldoptions)) {
195 $options = new stdClass;
516cf3eb 196 }
a6d46515 197 $options->question = $question->id;
198 $options->answer = $answer->id;
199 $options->tolerance = trim($question->tolerance[$key]);
200 $options->tolerancetype = trim($question->tolerancetype[$key]);
201 $options->correctanswerlength = trim($question->correctanswerlength[$key]);
202 $options->correctanswerformat = trim($question->correctanswerformat[$key]);
f34488b2 203
a6d46515 204 // Save options
205 if (isset($options->id)) { // reusing existing record
0bcf8b6f 206 $DB->update_record('question_calculated', $options);
a6d46515 207 } else { // new options
0bcf8b6f 208 $DB->insert_record('question_calculated', $options);
516cf3eb 209 }
210 }
211 }
a6d46515 212 // delete old answer records
213 if (!empty($oldanswers)) {
214 foreach($oldanswers as $oa) {
f34488b2 215 $DB->delete_records('question_answers', array('id' => $oa->id));
516cf3eb 216 }
a6d46515 217 }
218
219 // delete old answer records
220 if (!empty($oldoptions)) {
221 foreach($oldoptions as $oo) {
f34488b2 222 $DB->delete_records('question_calculated', array('id' => $oo->id));
516cf3eb 223 }
224 }
225
a6d46515 226
3f76dd52 227 if( isset($question->import_process)&&$question->import_process){
228 $this->import_datasets($question);
f34488b2 229 }
a6d46515 230 // Report any problems.
231 if (!empty($result->notice)) {
232 return $result;
233 }
516cf3eb 234 return true;
235 }
236
3f76dd52 237 function import_datasets($question){
f34488b2 238 global $DB;
3f76dd52 239 $n = count($question->dataset);
240 foreach ($question->dataset as $dataset) {
f34488b2 241 // name, type, option,
3f76dd52 242 $datasetdef = new stdClass();
243 $datasetdef->name = $dataset->name;
244 $datasetdef->type = 1 ;
245 $datasetdef->options = $dataset->distribution.':'.$dataset->min.':'.$dataset->max.':'.$dataset->length;
f34488b2 246 $datasetdef->itemcount=$dataset->itemcount;
3f76dd52 247 if ( $dataset->status =='private'){
248 $datasetdef->category = 0;
249 $todo='create' ;
250 }else if ($dataset->status =='shared' ){
f34488b2 251 if ($sharedatasetdefs = $DB->get_records_select(
3f76dd52 252 'question_dataset_definitions',
253 "type = '1'
f34488b2 254 AND name = ?
255 AND category = ?
256 ORDER BY id DESC;", array($dataset->name, $question->category)
3f76dd52 257 )) { // so there is at least one
258 $sharedatasetdef = array_shift($sharedatasetdefs);
259 if ( $sharedatasetdef->options == $datasetdef->options ){// identical so use it
260 $todo='useit' ;
261 $datasetdef =$sharedatasetdef ;
262 } else { // different so create a private one
263 $datasetdef->category = 0;
264 $todo='create' ;
f34488b2 265 }
cb024555 266 }else { // no so create one
267 $datasetdef->category =$question->category ;
268 $todo='create' ;
f34488b2 269 }
270 }
3f76dd52 271 if ( $todo=='create'){
bb4b6010 272 $datasetdef->id = $DB->insert_record( 'question_dataset_definitions', $datasetdef);
f34488b2 273 }
3f76dd52 274 // Create relation to the dataset:
275 $questiondataset = new stdClass;
276 $questiondataset->question = $question->id;
277 $questiondataset->datasetdefinition = $datasetdef->id;
bb4b6010 278 $DB->insert_record('question_datasets', $questiondataset);
3f76dd52 279 if ($todo=='create'){ // add the items
280 foreach ($dataset->datasetitem as $dataitem ){
281 $datasetitem = new stdClass;
282 $datasetitem->definition=$datasetdef->id ;
283 $datasetitem->itemnumber = $dataitem->itemnumber ;
284 $datasetitem->value = $dataitem->value ;
bb4b6010 285 $DB->insert_record('question_dataset_items', $datasetitem);
f34488b2 286 }
287 }
3f76dd52 288 }
289 }
f34488b2 290
fbe2cfea 291 function restore_session_and_responses(&$question, &$state) {
fef8f84e 292 global $OUTPUT;
6dbcacee 293 if (!preg_match('~^dataset([0-9]+)[^-]*-(.*)$~',
fbe2cfea 294 $state->responses[''], $regs)) {
fef8f84e 295 echo $OUTPUT->notification("Wrongly formatted raw response answer " .
fbe2cfea 296 "{$state->responses['']}! Could not restore session for " .
297 " question #{$question->id}.");
298 $state->options->datasetitem = 1;
299 $state->options->dataset = array();
300 $state->responses = array('' => '');
301 return false;
302 }
303
304 // Restore the chosen dataset
305 $state->options->datasetitem = $regs[1];
306 $state->options->dataset =
307 $this->pick_question_dataset($question,$state->options->datasetitem);
308 $state->responses = array('' => $regs[2]);
309 return true;
310 }
311
312 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
313 // Find out how many datasets are available
314 global $CFG, $DB;
315 if(!$maxnumber = (int)$DB->get_field_sql(
316 "SELECT MIN(a.itemcount)
317 FROM {question_dataset_definitions} a,
318 {question_datasets} b
319 WHERE b.question = ?
320 AND a.id = b.datasetdefinition", array($question->id))) {
321 print_error('cannotgetdsforquestion', 'question', '', $question->id);
322 }
1ee53ca9 323 $sql = "SELECT i.*
324 FROM {question_datasets} d,
325 {question_dataset_definitions} i
326 WHERE d.question = ?
327 AND d.datasetdefinition = i.id
328 AND i.category != 0
329 ";
330 if (!$question->options->synchronize || !$records = $DB->get_records_sql($sql, array($question->id))) {
331 $synchronize_calculated = false ;
332 }else {
333 $synchronize_calculated = true ;
334 }
fbe2cfea 335
336 // Choose a random dataset
1ee53ca9 337 if ( $synchronize_calculated === false ) {
f96e83d4 338 $state->options->datasetitem = rand(1, $maxnumber);
339 }else{
1ee53ca9 340 $state->options->datasetitem = intval( $maxnumber * substr($attempt->timestart,-2) /100 ) ;
341 if ($state->options->datasetitem < 1) {
342 $state->options->datasetitem =1 ;
343 } else if ($state->options->datasetitem > $maxnumber){
344 $state->options->datasetitem = $maxnumber ;
f96e83d4 345 }
1ee53ca9 346
f96e83d4 347 };
fbe2cfea 348 $state->options->dataset =
349 $this->pick_question_dataset($question,$state->options->datasetitem);
350 $state->responses = array('' => '');
351 return true;
352 }
353
354 function save_session_and_responses(&$question, &$state) {
355 global $DB;
356 $responses = 'dataset'.$state->options->datasetitem.'-'.
357 $state->responses[''];
358 // Set the legacy answer field
359 if (!$DB->set_field('question_states', 'answer', $responses, array('id'=> $state->id))) {
360 return false;
361 }
362 return true;
363 }
364
516cf3eb 365 function create_runtime_question($question, $form) {
366 $question = parent::create_runtime_question($question, $form);
367 $question->options->answers = array();
368 foreach ($form->answers as $key => $answer) {
369 $a->answer = trim($form->answer[$key]);
a6d46515 370 $a->fraction = $form->fraction[$key];//new
371 $a->tolerance = $form->tolerance[$key];
516cf3eb 372 $a->tolerancetype = $form->tolerancetype[$key];
373 $a->correctanswerlength = $form->correctanswerlength[$key];
374 $a->correctanswerformat = $form->correctanswerformat[$key];
375 $question->options->answers[] = clone($a);
376 }
377
378 return $question;
379 }
380
381 function validate_form($form) {
382 switch($form->wizardpage) {
383 case 'question':
1d9ed698 384 $calculatedmessages = array();
516cf3eb 385 if (empty($form->name)) {
1d9ed698 386 $calculatedmessages[] = get_string('missingname', 'quiz');
516cf3eb 387 }
388 if (empty($form->questiontext)) {
1d9ed698 389 $calculatedmessages[] = get_string('missingquestiontext', 'quiz');
516cf3eb 390 }
391 // Verify formulas
392 foreach ($form->answers as $key => $answer) {
393 if ('' === trim($answer)) {
1d9ed698 394 $calculatedmessages[] =
395 get_string('missingformula', 'quiz');
516cf3eb 396 }
1d9ed698 397 if ($formulaerrors =
398 qtype_calculated_find_formula_errors($answer)) {
399 $calculatedmessages[] = $formulaerrors;
516cf3eb 400 }
401 if (! isset($form->tolerance[$key])) {
402 $form->tolerance[$key] = 0.0;
403 }
404 if (! is_numeric($form->tolerance[$key])) {
1d9ed698 405 $calculatedmessages[] =
406 get_string('tolerancemustbenumeric', 'quiz');
516cf3eb 407 }
408 }
409
1d9ed698 410 if (!empty($calculatedmessages)) {
411 $errorstring = "The following errors were found:<br />";
412 foreach ($calculatedmessages as $msg) {
413 $errorstring .= $msg . '<br />';
414 }
415 print_error($errorstring);
416 }
417
516cf3eb 418 break;
419 default:
420 return parent::validate_form($form);
421 break;
422 }
423 return true;
424 }
fbe2cfea 425 function finished_edit_wizard(&$form) {
426 return isset($form->backtoquiz);
427 }
428 // This gets called by editquestion.php after the standard question is saved
429 function print_next_wizard_page(&$question, &$form, $course) {
430 global $CFG, $USER, $SESSION, $COURSE;
431
432 // Catch invalid navigation & reloads
433 if (empty($question->id) && empty($SESSION->calculated)) {
434 redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired.', 3);
435 }
436
437 // See where we're coming from
438 switch($form->wizardpage) {
439 case 'question':
440 require("$CFG->dirroot/question/type/calculated/datasetdefinitions.php");
441 break;
442 case 'datasetdefinitions':
443 case 'datasetitems':
444 require("$CFG->dirroot/question/type/calculated/datasetitems.php");
445 break;
446 default:
447 print_error('invalidwizardpage', 'question');
448 break;
449 }
450 }
451
452 // This gets called by question2.php after the standard question is saved
453 function &next_wizard_form($submiturl, $question, $wizardnow){
454 global $CFG, $SESSION, $COURSE;
455
456 // Catch invalid navigation & reloads
457 if (empty($question->id) && empty($SESSION->calculated)) {
458 redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired. Cannot get next wizard form.', 3);
459 }
460 if (empty($question->id)){
461 $question =& $SESSION->calculated->questionform;
462 }
463
464 // See where we're coming from
465 switch($wizardnow) {
466 case 'datasetdefinitions':
467 require("$CFG->dirroot/question/type/calculated/datasetdefinitions_form.php");
468 $mform =& new question_dataset_dependent_definitions_form("$submiturl?wizardnow=datasetdefinitions", $question);
469 break;
470 case 'datasetitems':
471 require("$CFG->dirroot/question/type/calculated/datasetitems_form.php");
472 $regenerate = optional_param('forceregeneration', 0, PARAM_BOOL);
473 $mform =& new question_dataset_dependent_items_form("$submiturl?wizardnow=datasetitems", $question, $regenerate);
474 break;
475 default:
476 print_error('invalidwizardpage', 'question');
477 break;
478 }
479
480 return $mform;
481 }
516cf3eb 482
fbe2cfea 483 /**
484 * This method should be overriden if you want to include a special heading or some other
485 * html on a question editing page besides the question editing form.
486 *
487 * @param question_edit_form $mform a child of question_edit_form
488 * @param object $question
489 * @param string $wizardnow is '' for first page.
490 */
491 function display_question_editing_page(&$mform, $question, $wizardnow){
492 switch ($wizardnow){
493 case '':
494 //on first page default display is fine
495 parent::display_question_editing_page($mform, $question, $wizardnow);
496 return;
497 break;
498 case 'datasetdefinitions':
499 print_heading_with_help(get_string("choosedatasetproperties", "quiz"), "questiondatasets", "quiz");
500 break;
501 case 'datasetitems':
502 print_heading_with_help(get_string("editdatasets", "quiz"), 'questiondatasets', "quiz");
503 break;
504 }
505
506
507 $mform->display();
508
509 }
510
511 /**
512 * This method prepare the $datasets in a format similar to dadatesetdefinitions_form.php
513 * so that they can be saved
514 * using the function save_dataset_definitions($form)
515 * when creating a new calculated question or
516 * whenediting an already existing calculated question
517 * or by function save_as_new_dataset_definitions($form, $initialid)
518 * when saving as new an already existing calculated question
519 *
520 * @param object $form
521 * @param int $questionfromid default = '0'
522 */
523 function preparedatasets(&$form , $questionfromid='0'){
524 // the dataset names present in the edit_question_form and edit_calculated_form are retrieved
525 $possibledatasets = $this->find_dataset_names($form->questiontext);
526 $mandatorydatasets = array();
527 foreach ($form->answers as $answer) {
528 $mandatorydatasets += $this->find_dataset_names($answer);
529 }
530 // if there are identical datasetdefs already saved in the original question.
531 // either when editing a question or saving as new
532 // they are retrieved using $questionfromid
533 if ($questionfromid!='0'){
534 $form->id = $questionfromid ;
535 }
536 $datasets = array();
537 $key = 0 ;
538 // always prepare the mandatorydatasets present in the answers
539 // the $options are not used here
540 foreach ($mandatorydatasets as $datasetname) {
541 if (!isset($datasets[$datasetname])) {
542 list($options, $selected) =
543 $this->dataset_options($form, $datasetname);
544 $datasets[$datasetname]='';
545 $form->dataset[$key]=$selected ;
546 $key++;
547 }
548 }
549 // do not prepare possibledatasets when creating a question
550 // they will defined and stored with datasetdefinitions_form.php
551 // the $options are not used here
552 if ($questionfromid!='0'){
553
554 foreach ($possibledatasets as $datasetname) {
555 if (!isset($datasets[$datasetname])) {
556 list($options, $selected) =
557 $this->dataset_options($form, $datasetname,false);
558 $datasets[$datasetname]='';
559 $form->dataset[$key]=$selected ;
560 $key++;
561 }
562 }
563 }
564 return $datasets ;
565 }
566
567 /**
568 * this version save the available data at the different steps of the question editing process
569 * without using global $SESSION as storage between steps
570 * at the first step $wizardnow = 'question'
571 * when creating a new question
572 * when modifying a question
573 * when copying as a new question
574 * the general parameters and answers are saved using parent::save_question
575 * then the datasets are prepared and saved
576 * at the second step $wizardnow = 'datasetdefinitions'
577 * the datadefs final type are defined as private, category or not a datadef
578 * at the third step $wizardnow = 'datasetitems'
579 * the datadefs parameters and the data items are created or defined
580 *
581 * @param object question
582 * @param object $form
583 * @param int $course
584 * @param PARAM_ALPHA $wizardnow should be added as we are coming from question2.php
585 */
586 function save_question($question, $form, $course) {
1ee53ca9 587 global $DB;
fbe2cfea 588 $wizardnow = optional_param('wizardnow', '', PARAM_ALPHA);
589 $id = optional_param('id', 0, PARAM_INT); // question id
590 // in case 'question'
591 // for a new question $form->id is empty
592 // when saving as new question
593 // $question->id = 0, $form is $data from question2.php
594 // and $data->makecopy is defined as $data->id is the initial question id
595 // edit case. If it is a new question we don't necessarily need to
596 // return a valid question object
597
598 // See where we're coming from
599 switch($wizardnow) {
600 case '' :
601 case 'question': // coming from the first page, creating the second
602 if (empty($form->id)) { // for a new question $form->id is empty
603 $question = parent::save_question($question, $form, $course);
604 //prepare the datasets using default $questionfromid
605 $this->preparedatasets($form);
606 $form->id = $question->id;
607 $this->save_dataset_definitions($form);
608 } else if (!empty($form->makecopy)){
609 $questionfromid = $form->id ;
610 $question = parent::save_question($question, $form, $course);
611 //prepare the datasets
612 $this->preparedatasets($form,$questionfromid);
613 $form->id = $question->id;
614 $this->save_as_new_dataset_definitions($form,$questionfromid );
615 } else {// editing a question
616 $question = parent::save_question($question, $form, $course);
617 //prepare the datasets
618 $this->preparedatasets($form,$question->id);
619 $form->id = $question->id;
620 $this->save_dataset_definitions($form);
621 }
622 break;
623 case 'datasetdefinitions':
1ee53ca9 624 // calculated options
625 $update = true ;
626 $options = $DB->get_record("question_calculated_options", array("question" => $question->id));
627 if (!$options) {
628 $update = false;
629 $options = new stdClass;
630 $options->question = $question->id;
631 }
bb02e41f 632 if($form->synchronize == 1 ){
633 $options->synchronize = $form->synchronize;
634 }else {
635 $options->synchronize = 0 ;
636 }
1ee53ca9 637 if ($update) {
638 if (!$DB->update_record("question_calculated_options", $options)) {
639 $result->error = "Could not update calculated question options! (id=$options->id)";
640 return $result;
641 }
642 } else {
643 if (!$DB->insert_record("question_calculated_options", $options)) {
644 $result->error = "Could not insert calculated question options!";
645 return $result;
646 }
647 }
fbe2cfea 648
649 $this->save_dataset_definitions($form);
650 break;
651 case 'datasetitems':
652 $this->save_dataset_items($question, $form);
450f1127 653 $this->save_question_calculated($question, $form);
fbe2cfea 654 break;
655 default:
656 print_error('invalidwizardpage', 'question');
657 break;
658 }
659 return $question;
660 }
516cf3eb 661 /**
662 * Deletes question from the question-type specific tables
663 *
664 * @return boolean Success/Failure
665 * @param object $question The question being deleted
666 */
90c3f310 667 function delete_question($questionid) {
f34488b2 668 global $DB;
669 $DB->delete_records("question_calculated", array("question" => $questionid));
1ee53ca9 670 if ( $DB->table_exists("question_calculated_options") ){
671 $DB->delete_records("question_calculated_options", array("question" => $questionid));
672 };
f34488b2 673 $DB->delete_records("question_numerical_units", array("question" => $questionid));
674 if ($datasets = $DB->get_records('question_datasets', array('question' => $questionid))) {
90c3f310 675 foreach ($datasets as $dataset) {
57db70af 676 if (!$DB->get_records_select(
677 'question_datasets',
678 "question != ?
679 AND datasetdefinition = ?;", array($questionid, $dataset->datasetdefinition))){
636bbc87 680 $DB->delete_records('question_dataset_definitions', array('id' => $dataset->datasetdefinition));
681 $DB->delete_records('question_dataset_items', array('definition' => $dataset->datasetdefinition));
682 }
90c3f310 683 }
684 }
f34488b2 685 $DB->delete_records("question_datasets", array("question" => $questionid));
516cf3eb 686 return true;
687 }
688
60b5ecd3 689 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
516cf3eb 690 // Substitute variables in questiontext before giving the data to the
691 // virtual type for printing
692 $virtualqtype = $this->get_virtual_qtype();
dde9b164 693 if($unit = $virtualqtype->get_default_numerical_unit($question)){
694 $unit = $unit->unit;
695 } else {
696 $unit = '';
f34488b2 697 }
516cf3eb 698 // We modify the question to look like a numerical question
a6d46515 699 $numericalquestion = fullclone($question);
516cf3eb 700 foreach ($numericalquestion->options->answers as $key => $answer) {
a6d46515 701 $answer = fullclone($numericalquestion->options->answers[$key]);
fbe2cfea 702 $numericalquestion->options->answers[$key]->answer = $this->substitute_variables_and_eval($answer->answer,
1d9ed698 703 $state->options->dataset);
516cf3eb 704 }
fbe2cfea 705 $numericalquestion->questiontext = $this->substitute_variables(
92186abc 706 $numericalquestion->questiontext, $state->options->dataset);
f34488b2 707 //evaluate the equations i.e {=5+4)
fd6b864f 708 $qtext = "";
709 $qtextremaining = $numericalquestion->questiontext ;
6dbcacee 710 while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
fd6b864f 711 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
712 $qtext =$qtext.$qtextsplits[0];
713 $qtextremaining = $qtextsplits[1];
714 if (empty($regs1[1])) {
715 $str = '';
716 } else {
717 if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
718 $str=$formulaerrors ;
f34488b2 719 }else {
720 eval('$str = '.$regs1[1].';');
fd6b864f 721 }
722 }
f34488b2 723 $qtext = $qtext.$str ;
724 }
fd6b864f 725 $numericalquestion->questiontext = $qtext.$qtextremaining ; // end replace equations
60b5ecd3 726 $virtualqtype->print_question_formulation_and_controls($numericalquestion, $state, $cmoptions, $options);
516cf3eb 727 }
516cf3eb 728 function grade_responses(&$question, &$state, $cmoptions) {
f34488b2 729 // Forward the grading to the virtual qtype
516cf3eb 730 // We modify the question to look like a numerical question
a6d46515 731 $numericalquestion = fullclone($question);
a6d46515 732 foreach ($numericalquestion->options->answers as $key => $answer) {
733 $answer = $numericalquestion->options->answers[$key]->answer; // for PHP 4.x
fbe2cfea 734 $numericalquestion->options->answers[$key]->answer = $this->substitute_variables_and_eval($answer,
516cf3eb 735 $state->options->dataset);
a6d46515 736 }
a6d46515 737 $virtualqtype = $this->get_virtual_qtype();
738 return $virtualqtype->grade_responses($numericalquestion, $state, $cmoptions) ;
516cf3eb 739 }
740
2280e147 741 function response_summary($question, $state, $length=80, $formatting=true) {
31d21f22 742 // The actual response is the bit after the hyphen
743 return substr($state->answer, strpos($state->answer, '-')+1, $length);
744 }
745
516cf3eb 746 // ULPGC ecastro
747 function check_response(&$question, &$state) {
748 // Forward the checking to the virtual qtype
749 // We modify the question to look like a numerical question
750 $numericalquestion = clone($question);
751 $numericalquestion->options = clone($question->options);
752 foreach ($question->options->answers as $key => $answer) {
753 $numericalquestion->options->answers[$key] = clone($answer);
754 }
755 foreach ($numericalquestion->options->answers as $key => $answer) {
756 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
fbe2cfea 757 $answer->answer = $this->substitute_variables_and_eval($answer->answer,
516cf3eb 758 $state->options->dataset);
759 }
a6d46515 760 $virtualqtype = $this->get_virtual_qtype();
761 return $virtualqtype->check_response($numericalquestion, $state) ;
516cf3eb 762 }
763
764 // ULPGC ecastro
765 function get_actual_response(&$question, &$state) {
766 // Substitute variables in questiontext before giving the data to the
767 // virtual type
768 $virtualqtype = $this->get_virtual_qtype();
769 $unit = $virtualqtype->get_default_numerical_unit($question);
770
771 // We modify the question to look like a numerical question
772 $numericalquestion = clone($question);
773 $numericalquestion->options = clone($question->options);
774 foreach ($question->options->answers as $key => $answer) {
775 $numericalquestion->options->answers[$key] = clone($answer);
776 }
777 foreach ($numericalquestion->options->answers as $key => $answer) {
778 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
fbe2cfea 779 $answer->answer = $this->substitute_variables_and_eval($answer->answer,
516cf3eb 780 $state->options->dataset);
781 // apply_unit
782 }
fbe2cfea 783 $numericalquestion->questiontext = $this->substitute_variables_and_eval(
516cf3eb 784 $numericalquestion->questiontext, $state->options->dataset);
785 $responses = $virtualqtype->get_all_responses($numericalquestion, $state);
786 $response = reset($responses->responses);
787 $correct = $response->answer.' : ';
788
789 $responses = $virtualqtype->get_actual_response($numericalquestion, $state);
790
791 foreach ($responses as $key=>$response){
792 $responses[$key] = $correct.$response;
793 }
794
795 return $responses;
796 }
797
798 function create_virtual_qtype() {
799 global $CFG;
aaae75b0 800 require_once("$CFG->dirroot/question/type/numerical/questiontype.php");
32a189d6 801 return new question_numerical_qtype();
516cf3eb 802 }
803
804 function supports_dataset_item_generation() {
805 // Calcualted support generation of randomly distributed number data
806 return true;
807 }
60b5ecd3 808 function custom_generator_tools_part(&$mform, $idx, $j){
809
810 $minmaxgrp = array();
87cd4f54 811 $minmaxgrp[] =& $mform->createElement('text', "calcmin[$idx]", get_string('calcmin', 'qtype_datasetdependent'));
812 $minmaxgrp[] =& $mform->createElement('text', "calcmax[$idx]", get_string('calcmax', 'qtype_datasetdependent'));
60b5ecd3 813 $mform->addGroup($minmaxgrp, 'minmaxgrp', get_string('minmax', 'qtype_datasetdependent'), ' - ', false);
87cd4f54 814 $mform->setType("calcmin[$idx]", PARAM_NUMBER);
815 $mform->setType("calcmax[$idx]", PARAM_NUMBER);
60b5ecd3 816
817 $precisionoptions = range(0, 10);
818 $mform->addElement('select', "calclength[$idx]", get_string('calclength', 'qtype_datasetdependent'), $precisionoptions);
819
820 $distriboptions = array('uniform' => get_string('uniform', 'qtype_datasetdependent'), 'loguniform' => get_string('loguniform', 'qtype_datasetdependent'));
821 $mform->addElement('select', "calcdistribution[$idx]", get_string('calcdistribution', 'qtype_datasetdependent'), $distriboptions);
822
823
60b5ecd3 824 }
825
826 function custom_generator_set_data($datasetdefs, $formdata){
827 $idx = 1;
828 foreach ($datasetdefs as $datasetdef){
6dbcacee 829 if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~', $datasetdef->options, $regs)) {
60b5ecd3 830 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
831 $formdata["calcdistribution[$idx]"] = $regs[1];
832 $formdata["calcmin[$idx]"] = $regs[2];
833 $formdata["calcmax[$idx]"] = $regs[3];
834 $formdata["calclength[$idx]"] = $regs[4];
835 }
836 $idx++;
837 }
838 return $formdata;
839 }
516cf3eb 840
841 function custom_generator_tools($datasetdef) {
44c64ad8 842 global $OUTPUT;
6dbcacee 843 if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
516cf3eb 844 $datasetdef->options, $regs)) {
845 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
846 for ($i = 0 ; $i<10 ; ++$i) {
847 $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
848 ? 'decimals'
849 : 'significantfigures'), 'quiz', $i);
850 }
44c64ad8 851 $select1 = html_select::make($lengthoptions, 'calclength[]', $regs[4], false);
852 $select1->nothingvalue = '';
853 $menu1 = $OUTPUT->select($select1);
854
855 $select2 = html_select::make(array('uniform' => get_string('uniform', 'quiz'),
856 'loguniform' => get_string('loguniform', 'quiz')), 'calcdistribution[]', $regs[1], false);
857 $select2->nothingvalue = '';
858 $menu2 = $OUTPUT->select($select2);
09275894 859 return '<input type="submit" onclick="'
d2ce367f 860 . "getElementById('addform').regenerateddefid.value='$defid'; return true;"
516cf3eb 861 .'" value="'. get_string('generatevalue', 'quiz') . '"/><br/>'
862 . '<input type="text" size="3" name="calcmin[]" '
863 . " value=\"$regs[2]\"/> &amp; <input name=\"calcmax[]\" "
864 . ' type="text" size="3" value="' . $regs[3] .'"/> '
44c64ad8 865 . $menu1 . '<br/>'
866 . $menu2;
516cf3eb 867 } else {
868 return '';
869 }
870 }
871
60b5ecd3 872
516cf3eb 873 function update_dataset_options($datasetdefs, $form) {
874 // Do we have informatin about new options???
875 if (empty($form->definition) || empty($form->calcmin)
876 || empty($form->calcmax) || empty($form->calclength)
877 || empty($form->calcdistribution)) {
a8d2a373 878 // I guess not
516cf3eb 879
880 } else {
881 // Looks like we just could have some new information here
60b5ecd3 882 $uniquedefs = array_values(array_unique($form->definition));
883 foreach ($uniquedefs as $key => $defid) {
516cf3eb 884 if (isset($datasetdefs[$defid])
60b5ecd3 885 && is_numeric($form->calcmin[$key+1])
886 && is_numeric($form->calcmax[$key+1])
887 && is_numeric($form->calclength[$key+1])) {
888 switch ($form->calcdistribution[$key+1]) {
516cf3eb 889 case 'uniform': case 'loguniform':
890 $datasetdefs[$defid]->options =
60b5ecd3 891 $form->calcdistribution[$key+1] . ':'
892 . $form->calcmin[$key+1] . ':'
893 . $form->calcmax[$key+1] . ':'
894 . $form->calclength[$key+1];
516cf3eb 895 break;
896 default:
fef8f84e 897 echo $OUTPUT->notification("Unexpected distribution ".$form->calcdistribution[$key+1]);
516cf3eb 898 }
899 }
900 }
901 }
902
903 // Look for empty options, on which we set default values
904 foreach ($datasetdefs as $defid => $def) {
905 if (empty($def->options)) {
906 $datasetdefs[$defid]->options = 'uniform:1.0:10.0:1';
907 }
908 }
909 return $datasetdefs;
910 }
911
450f1127 912 function save_question_calculated($question, $fromform){
913 global $DB;
914
915 foreach ($question->options->answers as $key => $answer) {
916 if ($options = $DB->get_record('question_calculated', array('answer' => $key))) {
917 $options->tolerance = trim($fromform->tolerance[$key]);
918 $options->tolerancetype = trim($fromform->tolerancetype[$key]);
919 $options->correctanswerlength = trim($fromform->correctanswerlength[$key]);
920 $options->correctanswerformat = trim($fromform->correctanswerformat[$key]);
0bcf8b6f 921 $DB->update_record('question_calculated', $options);
450f1127 922 }
923 }
924 }
925
a2155a7b 926 /**
927 * This function get the dataset items using id as unique parameter and return an
928 * array with itemnumber as index sorted ascendant
929 * If the multiple records with the same itemnumber exist, only the newest one
930 * i.e with the greatest id is used, the others are ignored but not deleted.
931 * MDL-19210
932 */
933 function get_database_dataset_items($definition){
934 global $CFG, $DB;
935 $databasedataitems = $DB->get_records_sql( // Use number as key!!
936 " SELECT id , itemnumber, definition, value
4ff9ba7d 937 FROM {question_dataset_items}
a2155a7b 938 WHERE definition = $definition order by id DESC ", array($definition));
939 $dataitems = Array();
940 foreach($databasedataitems as $id => $dataitem ){
941 if (!isset($dataitems[$dataitem->itemnumber])){
942 $dataitems[$dataitem->itemnumber] = $dataitem ;
943 }else {
944 // deleting the unused records could be added here
945 }
946 }
947 ksort($dataitems);
948 return $dataitems ;
949 }
450f1127 950
60b5ecd3 951 function save_dataset_items($question, $fromform){
f34488b2 952 global $CFG, $DB;
451373ed 953 // max datasets = 100 items
c31f631b 954 $max100 = 100 ;
f34488b2 955 if(isset($fromform->nextpageparam["forceregeneration"])) {
d650e1a3 956 $regenerate = $fromform->nextpageparam["forceregeneration"];
957 }else{
958 $regenerate = 0 ;
959 }
60b5ecd3 960 if (empty($question->options)) {
961 $this->get_question_options($question);
962 }
963 //get the old datasets for this question
964 $datasetdefs = $this->get_dataset_definitions($question->id, array());
965 // Handle generator options...
966 $olddatasetdefs = fullclone($datasetdefs);
967 $datasetdefs = $this->update_dataset_options($datasetdefs, $fromform);
968 $maxnumber = -1;
969 foreach ($datasetdefs as $defid => $datasetdef) {
970 if (isset($datasetdef->id)
971 && $datasetdef->options != $olddatasetdefs[$defid]->options) {
972 // Save the new value for options
f34488b2 973 $DB->update_record('question_dataset_definitions', $datasetdef);
60b5ecd3 974
975 }
976 // Get maxnumber
977 if ($maxnumber == -1 || $datasetdef->itemcount < $maxnumber) {
978 $maxnumber = $datasetdef->itemcount;
979 }
980 }
981 // Handle adding and removing of dataset items
982 $i = 1;
a8d2a373 983 ksort($fromform->definition);
60b5ecd3 984 foreach ($fromform->definition as $key => $defid) {
985 //if the delete button has not been pressed then skip the datasetitems
986 //in the 'add item' part of the form.
987 if ((!isset($fromform->addbutton)) && ($i > (count($datasetdefs)*$maxnumber))) {
988 break;
989 }
990 $addeditem = new stdClass();
991 $addeditem->definition = $datasetdefs[$defid]->id;
992 $addeditem->value = $fromform->number[$i];
993 $addeditem->itemnumber = ceil($i / count($datasetdefs));
994
995 if ($fromform->itemid[$i]) {
996 // Reuse any previously used record
997 $addeditem->id = $fromform->itemid[$i];
bb4b6010 998 $DB->update_record('question_dataset_items', $addeditem);
60b5ecd3 999 } else {
bb4b6010 1000 $DB->insert_record('question_dataset_items', $addeditem);
60b5ecd3 1001 }
1002
1003 $i++;
1004 }
450f1127 1005 if (isset($addeditem->itemnumber) && $maxnumber < $addeditem->itemnumber){
60b5ecd3 1006 $maxnumber = $addeditem->itemnumber;
1007 foreach ($datasetdefs as $key => $newdef) {
1008 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
1009 $newdef->itemcount = $maxnumber;
1010 // Save the new value for options
f34488b2 1011 $DB->update_record('question_dataset_definitions', $newdef);
60b5ecd3 1012 }
1013 }
1014 }
451373ed 1015 // adding supplementary items
1016 $numbertoadd =0;
1017 if (isset($fromform->addbutton) && $fromform->selectadd > 1 && $maxnumber < $max100 ) {
c31f631b 1018 $numbertoadd =$fromform->selectadd-1 ;
1019 if ( $max100 - $maxnumber < $numbertoadd ) {
1020 $numbertoadd = $max100 - $maxnumber ;
1021 }
451373ed 1022 //add the other items.
1023 // Generate a new dataset item (or reuse an old one)
1024 foreach ($datasetdefs as $defid => $datasetdef) {
a2155a7b 1025 // in case that for category datasets some new items has been added
1026 // get actual values
451373ed 1027 if (isset($datasetdef->id)) {
a2155a7b 1028 $datasetdefs[$defid]->items = $this->get_database_dataset_items($datasetdef->id);
451373ed 1029 }
c31f631b 1030 for ($numberadded =$maxnumber+1 ; $numberadded <= $maxnumber+$numbertoadd ; $numberadded++){
ad3a4aa6 1031 if (isset($datasetdefs[$defid]->items[$numberadded]) ){
1032 // in case of regenerate it modifies the already existing record
1033 if ( $regenerate ) {
1034 $datasetitem = new stdClass;
1035 $datasetitem->id = $datasetdefs[$defid]->items[$numberadded]->id;
1036 $datasetitem->definition = $datasetdef->id ;
1037 $datasetitem->itemnumber = $numberadded;
1038 $datasetitem->value = $this->generate_dataset_item($datasetdef->options);
bb4b6010 1039 $DB->update_record('question_dataset_items', $datasetitem);
ad3a4aa6 1040 }
1041 //if not regenerate do nothing as there is already a record
451373ed 1042 } else {
1043 $datasetitem = new stdClass;
1044 $datasetitem->definition = $datasetdef->id ;
1045 $datasetitem->itemnumber = $numberadded;
1046 if ($this->supports_dataset_item_generation()) {
1047 $datasetitem->value = $this->generate_dataset_item($datasetdef->options);
1048 } else {
1049 $datasetitem->value = '';
1050 }
bb4b6010 1051 $DB->insert_record('question_dataset_items', $datasetitem);
c31f631b 1052 }
1053 }//for number added
1054 }// datasetsdefs end
1055 $maxnumber += $numbertoadd ;
1056 foreach ($datasetdefs as $key => $newdef) {
451373ed 1057 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
1058 $newdef->itemcount = $maxnumber;
1059 // Save the new value for options
f34488b2 1060 $DB->update_record('question_dataset_definitions', $newdef);
451373ed 1061 }
1062 }
f34488b2 1063 }
451373ed 1064
60b5ecd3 1065 if (isset($fromform->deletebutton)) {
451373ed 1066 if(isset($fromform->selectdelete)) $newmaxnumber = $maxnumber-$fromform->selectdelete ;
1067 else $newmaxnumber = $maxnumber-1 ;
1068 if ($newmaxnumber < 0 ) $newmaxnumber = 0 ;
60b5ecd3 1069 foreach ($datasetdefs as $datasetdef) {
1070 if ($datasetdef->itemcount == $maxnumber) {
f34488b2 1071 $datasetdef->itemcount= $newmaxnumber ;
bb4b6010 1072 $DB->update_record('question_dataset_definitions', $datasetdef);
60b5ecd3 1073 }
1074 }
451373ed 1075 }
60b5ecd3 1076 }
516cf3eb 1077 function generate_dataset_item($options) {
6dbcacee 1078 if (!preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
516cf3eb 1079 $options, $regs)) {
1080 // Unknown options...
1081 return false;
1082 }
1083 if ($regs[1] == 'uniform') {
1084 $nbr = $regs[2] + ($regs[3]-$regs[2])*mt_rand()/mt_getrandmax();
87cd4f54 1085 return sprintf("%.".$regs[4]."f",$nbr);
516cf3eb 1086
1087 } else if ($regs[1] == 'loguniform') {
1088 $log0 = log(abs($regs[2])); // It would have worked the other way to
f34488b2 1089 $nbr = exp($log0 + (log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax());
87cd4f54 1090 return sprintf("%.".$regs[4]."f",$nbr);
f34488b2 1091
516cf3eb 1092 } else {
0b4f4187 1093 print_error('disterror', 'question', '', $regs[1]);
516cf3eb 1094 }
1095 return '';
1096 }
1097
1098 function comment_header($question) {
1099 //$this->get_question_options($question);
450f1127 1100 $strheader = array();
516cf3eb 1101 $delimiter = '';
60b5ecd3 1102
1103 $answers = $question->options->answers;
1104
450f1127 1105 foreach ($answers as $key => $answer) {
516cf3eb 1106 if (is_string($answer)) {
1107 $strheader .= $delimiter.$answer;
1108 } else {
1109 $strheader .= $delimiter.$answer->answer;
1110 }
c4d18bdd 1111 $delimiter = '<br/><br/><br/>';
516cf3eb 1112 }
1113 return $strheader;
1114 }
1115
450f1127 1116 function comment_on_datasetitems($questionid, $answers,$data, $number) {
f34488b2 1117 global $DB;
450f1127 1118 $comment = new stdClass;
1119 $comment->stranswers = array();
1120 $comment->outsidelimit = false ;
1121 $comment->answers = array();
516cf3eb 1122 /// Find a default unit:
450f1127 1123 if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units', array('question'=> $questionid, 'multiplier' => 1.0))) {
516cf3eb 1124 $unit = $unit->unit;
1125 } else {
1126 $unit = '';
1127 }
1128
450f1127 1129 $answers = fullclone($answers);
516cf3eb 1130 $strmin = get_string('min', 'quiz');
1131 $strmax = get_string('max', 'quiz');
1132 $errors = '';
1133 $delimiter = ': ';
1134 $virtualqtype = $this->get_virtual_qtype();
450f1127 1135 foreach ($answers as $key => $answer) {
fbe2cfea 1136 $formula = $this->substitute_variables($answer->answer,$data);
1d9ed698 1137 $formattedanswer = qtype_calculated_calculate_answer(
516cf3eb 1138 $answer->answer, $data, $answer->tolerance,
1139 $answer->tolerancetype, $answer->correctanswerlength,
1140 $answer->correctanswerformat, $unit);
078cc3f0 1141 if ( $formula === '*'){
450f1127 1142 $answer->min = ' ';
1143 $formattedanswer->answer = $answer->answer ;
078cc3f0 1144 }else {
1145 eval('$answer->answer = '.$formula.';') ;
1146 $virtualqtype->get_tolerance_interval($answer);
1147 }
1d9ed698 1148 if ($answer->min === '') {
516cf3eb 1149 // This should mean that something is wrong
450f1127 1150 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
1151 } else if ($formula === '*'){
1152 $comment->stranswers[$key] = $formula.' = '.get_string('anyvalue','qtype_calculated').'<br/><br/><br/>';
1153 }else{
1154 $comment->stranswers[$key]= $formula.' = '.$formattedanswer->answer.'<br/>' ;
1155 $comment->stranswers[$key] .= $strmin. $delimiter.$answer->min.'---';
1156 $comment->stranswers[$key] .= $strmax.$delimiter.$answer->max;
1157 $comment->stranswers[$key] .='<br/>';
1d9ed698 1158 $correcttrue->correct = $formattedanswer->answer ;
1159 $correcttrue->true = $answer->answer ;
0d03d6be 1160 if ($formattedanswer->answer < $answer->min || $formattedanswer->answer > $answer->max){
450f1127 1161 $comment->outsidelimit = true ;
1162 $comment->answers[$key] = $key;
1163 $comment->stranswers[$key] .=get_string('trueansweroutsidelimits','qtype_calculated',$correcttrue);//<span class="error">ERROR True answer '..' outside limits</span>';
1d9ed698 1164 }else {
450f1127 1165 $comment->stranswers[$key] .=get_string('trueanswerinsidelimits','qtype_calculated',$correcttrue);//' True answer :'.$calculated->trueanswer.' inside limits';
0d03d6be 1166 }
450f1127 1167 $comment->stranswers[$key] .='';
516cf3eb 1168 }
1169 }
450f1127 1170 return fullclone($comment);
516cf3eb 1171 }
1172
1173 function tolerance_types() {
1174 return array('1' => get_string('relative', 'quiz'),
1175 '2' => get_string('nominal', 'quiz'),
1176 '3' => get_string('geometric', 'quiz'));
1177 }
1178
fd0973cc 1179 function dataset_options($form, $name, $mandatory=true,$renameabledatasets=false) {
516cf3eb 1180 // Takes datasets from the parent implementation but
1181 // filters options that are currently not accepted by calculated
1182 // It also determines a default selection...
f34488b2 1183 //$renameabledatasets not implemented anmywhere
fbe2cfea 1184 list($options, $selected) = $this->dataset_options_from_database($form, $name,'','qtype_calculated');
fd0973cc 1185 // list($options, $selected) = $this->dataset_optionsa($form, $name);
1186
516cf3eb 1187 foreach ($options as $key => $whatever) {
6dbcacee 1188 if (!preg_match('~^1-~', $key) && $key != '0') {
516cf3eb 1189 unset($options[$key]);
1190 }
1191 }
1192 if (!$selected) {
f34488b2 1193 if ($mandatory){
fbe2cfea 1194 $selected = "1-0-$name"; // Default
fd0973cc 1195 }else {
1196 $selected = "0"; // Default
f34488b2 1197 }
516cf3eb 1198 }
1199 return array($options, $selected);
1200 }
1201
1202 function construct_dataset_menus($form, $mandatorydatasets,
1203 $optionaldatasets) {
44c64ad8 1204 global $OUTPUT;
516cf3eb 1205 $datasetmenus = array();
1206 foreach ($mandatorydatasets as $datasetname) {
1207 if (!isset($datasetmenus[$datasetname])) {
1208 list($options, $selected) =
1209 $this->dataset_options($form, $datasetname);
1210 unset($options['0']); // Mandatory...
44c64ad8 1211 $datasetmenus[$datasetname] = $OUTPUT->select(html_select::make($options, 'dataset[]', $selected, false));
516cf3eb 1212 }
1213 }
1214 foreach ($optionaldatasets as $datasetname) {
1215 if (!isset($datasetmenus[$datasetname])) {
1216 list($options, $selected) =
1217 $this->dataset_options($form, $datasetname);
44c64ad8 1218 $datasetmenus[$datasetname] = $OUTPUT->select(html_select::make($options, 'dataset[]', $selected, false));
516cf3eb 1219 }
1220 }
1221 return $datasetmenus;
1222 }
1223
20bf2c1a 1224 function print_question_grading_details(&$question, &$state, &$cmoptions, &$options) {
1225 $virtualqtype = $this->get_virtual_qtype();
1226 $virtualqtype->print_question_grading_details($question, $state, $cmoptions, $options) ;
1227 }
1228
516cf3eb 1229 function get_correct_responses(&$question, &$state) {
1230 $virtualqtype = $this->get_virtual_qtype();
dde9b164 1231 if($unit = $virtualqtype->get_default_numerical_unit($question)){
1232 $unit = $unit->unit;
1233 } else {
1234 $unit = '';
f34488b2 1235 }
516cf3eb 1236 foreach ($question->options->answers as $answer) {
1237 if (((int) $answer->fraction) === 1) {
7518b645 1238 $answernumerical = qtype_calculated_calculate_answer(
516cf3eb 1239 $answer->answer, $state->options->dataset, $answer->tolerance,
1240 $answer->tolerancetype, $answer->correctanswerlength,
dde9b164 1241 $answer->correctanswerformat, $unit);
516cf3eb 1242 return array('' => $answernumerical->answer);
1243 }
1244 }
1245 return null;
1246 }
1247
1248 function substitute_variables($str, $dataset) {
1ee53ca9 1249 // testing for wrong numerical values
1250 // all calculations used this function so testing here should be OK
fbe2cfea 1251
1252 foreach ($dataset as $name => $value) {
1ee53ca9 1253 $val = $value ;
1254 if(! is_numeric($val)){
1255 $a = new stdClass;
1256 $a->name = '{'.$name.'}' ;
1257 $a->value = $value ;
1258 echo $OUTPUT->notification(get_string('notvalidnumber','qtype_calculated',$a));
1259 $val = 1.0 ;
1260 }
1261 if($val < 0 ){
1262 $str = str_replace('{'.$name.'}', '('.$val.')', $str);
fbe2cfea 1263 } else {
1ee53ca9 1264 $str = str_replace('{'.$name.'}', $val, $str);
fbe2cfea 1265 }
1266 }
1267 return $str;
1268 }
1269
1270 function substitute_variables_and_eval($str, $dataset) {
1271 $formula = $this->substitute_variables($str, $dataset) ;
1272 if ($error = qtype_calculated_find_formula_errors($formula)) {
516cf3eb 1273 return $error;
1274 }
1275 /// Calculate the correct answer
1276 if (empty($formula)) {
1277 $str = '';
078cc3f0 1278 } else if ($formula === '*'){
1279 $str = '*';
516cf3eb 1280 } else {
f34488b2 1281 eval('$str = '.$formula.';');
516cf3eb 1282 }
1283 return $str;
1284 }
f34488b2 1285
fbe2cfea 1286 function get_dataset_definitions($questionid, $newdatasets) {
1287 global $DB;
1288 //get the existing datasets for this question
1289 $datasetdefs = array();
1290 if (!empty($questionid)) {
1291 global $CFG;
1292 $sql = "SELECT i.*
1293 FROM {question_datasets} d,
1294 {question_dataset_definitions} i
1295 WHERE d.question = ?
1296 AND d.datasetdefinition = i.id
1297 ";
1298 if ($records = $DB->get_records_sql($sql, array($questionid))) {
1299 foreach ($records as $r) {
1300 $datasetdefs["$r->type-$r->category-$r->name"] = $r;
1301 }
1302 }
1303 }
1304
1305 foreach ($newdatasets as $dataset) {
1306 if (!$dataset) {
1307 continue; // The no dataset case...
1308 }
1309
1310 if (!isset($datasetdefs[$dataset])) {
1311 //make new datasetdef
1312 list($type, $category, $name) = explode('-', $dataset, 3);
1313 $datasetdef = new stdClass;
1314 $datasetdef->type = $type;
1315 $datasetdef->name = $name;
1316 $datasetdef->category = $category;
1317 $datasetdef->itemcount = 0;
1318 $datasetdef->options = 'uniform:1.0:10.0:1';
1319 $datasetdefs[$dataset] = clone($datasetdef);
1320 }
1321 }
1322 return $datasetdefs;
1323 }
1324
1325 function save_dataset_definitions($form) {
1326 global $DB;
1ee53ca9 1327 // save synchronize
1328
fbe2cfea 1329 // Save datasets
1330 $datasetdefinitions = $this->get_dataset_definitions($form->id, $form->dataset);
1331 $tmpdatasets = array_flip($form->dataset);
1332 $defids = array_keys($datasetdefinitions);
1333 foreach ($defids as $defid) {
1334 $datasetdef = &$datasetdefinitions[$defid];
1335 if (isset($datasetdef->id)) {
1336 if (!isset($tmpdatasets[$defid])) {
1337 // This dataset is not used any more, delete it
1338 $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $datasetdef->id));
1339 if ($datasetdef->category == 0) { // Question local dataset
1340 $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id));
1341 $DB->delete_records('question_dataset_items', array('definition' => $datasetdef->id));
1342 }
1343 }
1344 // This has already been saved or just got deleted
1345 unset($datasetdefinitions[$defid]);
1346 continue;
1347 }
1348
bb4b6010 1349 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
fbe2cfea 1350
1351 if (0 != $datasetdef->category) {
1352 // We need to look for already existing
1353 // datasets in the category.
1354 // By first creating the datasetdefinition above we
1355 // can manage to automatically take care of
1356 // some possible realtime concurrence
1357 if ($olderdatasetdefs = $DB->get_records_select( 'question_dataset_definitions',
1358 "type = ?
1359 AND name = ?
1360 AND category = ?
1361 AND id < ?
1362 ORDER BY id DESC", array($datasetdef->type, $datasetdef->name, $datasetdef->category, $datasetdef->id))) {
1363
1364 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1365 $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id));
1366 $datasetdef = $olderdatasetdef;
1367 }
1368 }
1369 }
1370
1371 // Create relation to this dataset:
1372 $questiondataset = new stdClass;
1373 $questiondataset->question = $form->id;
1374 $questiondataset->datasetdefinition = $datasetdef->id;
bb4b6010 1375 $DB->insert_record('question_datasets', $questiondataset);
fbe2cfea 1376 unset($datasetdefinitions[$defid]);
1377 }
1378
1379 // Remove local obsolete datasets as well as relations
1380 // to datasets in other categories:
1381 if (!empty($datasetdefinitions)) {
1382 foreach ($datasetdefinitions as $def) {
1383 $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $def->id));
1384
1385 if ($def->category == 0) { // Question local dataset
1386 $DB->delete_records('question_dataset_definitions', array('id' => $def->id));
1387 $DB->delete_records('question_dataset_items', array('definition' => $def->id));
1388 }
1389 }
1390 }
1391 }
1392 /** This function create a copy of the datasets ( definition and dataitems)
1393 * from the preceding question if they remain in the new question
1394 * otherwise its create the datasets that have been added as in the
1395 * save_dataset_definitions()
1396 */
1397 function save_as_new_dataset_definitions($form, $initialid) {
1398 global $CFG, $DB;
1399 // Get the datasets from the intial question
1400 $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset);
1401 // $tmpdatasets contains those of the new question
1402 $tmpdatasets = array_flip($form->dataset);
1403 $defids = array_keys($datasetdefinitions);// new datasets
1404 foreach ($defids as $defid) {
1405 $datasetdef = &$datasetdefinitions[$defid];
1406 if (isset($datasetdef->id)) {
1407 // This dataset exist in the initial question
1408 if (!isset($tmpdatasets[$defid])) {
1409 // do not exist in the new question so ignore
1410 unset($datasetdefinitions[$defid]);
1411 continue;
1412 }
1413 // create a copy but not for category one
1414 if (0 == $datasetdef->category) {
1415 $olddatasetid = $datasetdef->id ;
1416 $olditemcount = $datasetdef->itemcount ;
1417 $datasetdef->itemcount =0;
bb4b6010 1418 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
fbe2cfea 1419 //copy the dataitems
a2155a7b 1420 $olditems = $this->get_database_dataset_items($olddatasetid);
fbe2cfea 1421 if (count($olditems) > 0 ) {
1422 $itemcount = 0;
1423 foreach($olditems as $item ){
1424 $item->definition = $datasetdef->id;
bb4b6010 1425 $DB->insert_record('question_dataset_items', $item);
1426 $itemcount++;
fbe2cfea 1427 }
1428 //update item count
1429 $datasetdef->itemcount =$itemcount;
1430 $DB->update_record('question_dataset_definitions', $datasetdef);
1431 } // end of copy the dataitems
1432 }// end of copy the datasetdef
1433 // Create relation to the new question with this
1434 // copy as new datasetdef from the initial question
1435 $questiondataset = new stdClass;
1436 $questiondataset->question = $form->id;
1437 $questiondataset->datasetdefinition = $datasetdef->id;
bb4b6010 1438 $DB->insert_record('question_datasets', $questiondataset);
fbe2cfea 1439 unset($datasetdefinitions[$defid]);
1440 continue;
1441 }// end of datasetdefs from the initial question
1442 // really new one code similar to save_dataset_definitions()
bb4b6010 1443 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
fbe2cfea 1444
1445 if (0 != $datasetdef->category) {
1446 // We need to look for already existing
1447 // datasets in the category.
1448 // By first creating the datasetdefinition above we
1449 // can manage to automatically take care of
1450 // some possible realtime concurrence
1451 if ($olderdatasetdefs = $DB->get_records_select(
1452 'question_dataset_definitions',
1453 "type = ?
1454 AND name = ?
1455 AND category = ?
1456 AND id < ?
1457 ORDER BY id DESC", array($datasetdef->type, $datasetdef->name, $datasetdef->category, $datasetdef->id))) {
1458
1459 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1460 $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id));
1461 $datasetdef = $olderdatasetdef;
1462 }
1463 }
1464 }
1465
1466 // Create relation to this dataset:
1467 $questiondataset = new stdClass;
1468 $questiondataset->question = $form->id;
1469 $questiondataset->datasetdefinition = $datasetdef->id;
bb4b6010 1470 $DB->insert_record('question_datasets', $questiondataset);
fbe2cfea 1471 unset($datasetdefinitions[$defid]);
1472 }
1473
1474 // Remove local obsolete datasets as well as relations
1475 // to datasets in other categories:
1476 if (!empty($datasetdefinitions)) {
1477 foreach ($datasetdefinitions as $def) {
1478 $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $def->id));
1479
1480 if ($def->category == 0) { // Question local dataset
1481 $DB->delete_records('question_dataset_definitions', array('id' => $def->id));
1482 $DB->delete_records('question_dataset_items', array('definition' => $def->id));
1483 }
1484 }
1485 }
1486 }
1487
1488/// Dataset functionality
1489 function pick_question_dataset($question, $datasetitem) {
1490 // Select a dataset in the following format:
1491 // An array indexed by the variable names (d.name) pointing to the value
1492 // to be substituted
1493 global $CFG, $DB;
a2155a7b 1494 if (!$dataitems = $DB->get_records_sql(
1495 "SELECT i.id, d.name, i.value
fbe2cfea 1496 FROM {question_dataset_definitions} d,
1497 {question_dataset_items} i,
1498 {question_datasets} q
1499 WHERE q.question = ?
1500 AND q.datasetdefinition = d.id
1501 AND d.id = i.definition
a2155a7b 1502 AND i.itemnumber = ? ORDER by i.id DESC ", array($question->id, $datasetitem))) {
fbe2cfea 1503 print_error('cannotgetdsfordependent', 'question', '', array($question->id, $datasetitem));
1504 }
a2155a7b 1505 $dataset = Array();
1506 foreach($dataitems as $id => $dataitem ){
1507 if (!isset($dataset[$dataitem->name])){
1508 $dataset[$dataitem->name] = $dataitem->value ;
1509 }else {
1510 // deleting the unused records could be added here
1511 }
1512 }
fbe2cfea 1513 return $dataset;
1514 }
1515
1516 function dataset_options_from_database($form, $name,$prefix='',$langfile='quiz') {
1517
1518 // First options - it is not a dataset...
1519 $options['0'] = get_string($prefix.'nodataset', $langfile);
1520
1521 // Construct question local options
1522 global $CFG, $DB;
cae0b24a 1523 $type = 1 ; // only type = 1 (i.e. old 'LITERAL') has ever been used
1524 if ( ! $currentdatasetdef = $DB->get_record_sql(
fbe2cfea 1525 "SELECT a.*
1526 FROM {question_dataset_definitions} a,
1527 {question_datasets} b
1528 WHERE a.id = b.datasetdefinition
cae0b24a 1529 AND a.type = '1'
fbe2cfea 1530 AND b.question = ?
cae0b24a 1531 AND a.name = ?", array($form->id, $name))){
1532 $currentdatasetdef->type = '0';
1533 };
1534 $key = "$type-0-$name";
1535 if ($currentdatasetdef->type == $type
1536 and $currentdatasetdef->category == 0) {
1537 $options[$key] = get_string($prefix."keptlocal$type", $langfile);
1538 } else {
1539 $options[$key] = get_string($prefix."newlocal$type", $langfile);
fbe2cfea 1540 }
fbe2cfea 1541 // Construct question category options
1542 $categorydatasetdefs = $DB->get_records_sql(
cae0b24a 1543 "SELECT b.question, a.*
1544 FROM {question_datasets} b,
1545 {question_dataset_definitions} a
fbe2cfea 1546 WHERE a.id = b.datasetdefinition
cae0b24a 1547 AND a.type = '1'
fbe2cfea 1548 AND a.category = ?
1549 AND a.name = ?", array($form->category, $name));
cae0b24a 1550 $type = 1 ;
1551 $key = "$type-$form->category-$name";
cae0b24a 1552 if (!empty($categorydatasetdefs)){ // there is at least one with the same name
1553 if (isset($categorydatasetdefs[$form->id])) {// it is already used by this question
fbe2cfea 1554 $options[$key] = get_string($prefix."keptcategory$type", $langfile);
1555 } else {
1556 $options[$key] = get_string($prefix."existingcategory$type", $langfile);
1557 }
cae0b24a 1558 } else {
1559 $options[$key] = get_string($prefix."newcategory$type", $langfile);
fbe2cfea 1560 }
fbe2cfea 1561 // All done!
1562 return array($options, $currentdatasetdef->type
1563 ? "$currentdatasetdef->type-$currentdatasetdef->category-$name"
1564 : '');
1565 }
1566
1567 function find_dataset_names($text) {
1568 /// Returns the possible dataset names found in the text as an array
1569 /// The array has the dataset name for both key and value
1570 $datasetnames = array();
6dbcacee 1571 while (preg_match('~\\{([[:alpha:]][^>} <{"\']*)\\}~', $text, $regs)) {
fbe2cfea 1572 $datasetnames[$regs[1]] = $regs[1];
1573 $text = str_replace($regs[0], '', $text);
1574 }
1575 return $datasetnames;
1576 }
1577
fd0973cc 1578 /**
1579 * This function retrieve the item count of the available category shareable
f34488b2 1580 * wild cards that is added as a comment displayed when a wild card with
fd0973cc 1581 * the same name is displayed in datasetdefinitions_form.php
f34488b2 1582 */
fd0973cc 1583 function get_dataset_definitions_category($form) {
f34488b2 1584 global $CFG, $DB;
fd0973cc 1585 $datasetdefs = array();
1586 $lnamemax = 30;
f34488b2 1587 if (!empty($form->category)) {
fd0973cc 1588 $sql = "SELECT i.*,d.*
f34488b2 1589 FROM {question_datasets} d,
1590 {question_dataset_definitions} i
fd0973cc 1591 WHERE i.id = d.datasetdefinition
f34488b2 1592 AND i.category = ?
fd0973cc 1593 ;
1594 ";
f34488b2 1595 if ($records = $DB->get_records_sql($sql, array($form->category))) {
fd0973cc 1596 foreach ($records as $r) {
1597 if ( !isset ($datasetdefs["$r->name"])) $datasetdefs["$r->name"] = $r->itemcount;
1598 }
1599 }
f34488b2 1600 }
fd0973cc 1601 return $datasetdefs ;
f34488b2 1602 }
fd0973cc 1603
1604 /**
1605 * This function build a table showing the available category shareable
1606 * wild cards, their name, their definition (Min, Max, Decimal) , the item count
1607 * and the name of the question where they are used.
f34488b2 1608 * This table is intended to be add before the question text to help the user use
fd0973cc 1609 * these wild cards
f34488b2 1610 */
1611
fd0973cc 1612 function print_dataset_definitions_category($form) {
f34488b2 1613 global $CFG, $DB;
fd0973cc 1614 $datasetdefs = array();
1615 $lnamemax = 22;
1616 $namestr =get_string('name', 'quiz');
1617 $minstr=get_string('min', 'quiz');
1618 $maxstr=get_string('max', 'quiz');
1619 $rangeofvaluestr=get_string('minmax','qtype_datasetdependent');
1620 $questionusingstr = get_string('usedinquestion','qtype_calculated');
fd0973cc 1621 $itemscountstr = get_string('itemscount','qtype_datasetdependent');
1622 $text ='';
f34488b2 1623 if (!empty($form->category)) {
08121fad 1624 list($category) = explode(',', $form->category);
fd0973cc 1625 $sql = "SELECT i.*,d.*
f34488b2 1626 FROM {question_datasets} d,
1627 {question_dataset_definitions} i
fd0973cc 1628 WHERE i.id = d.datasetdefinition
f34488b2 1629 AND i.category = ?;
fd0973cc 1630 " ;
f34488b2 1631 if ($records = $DB->get_records_sql($sql, array($category))) {
fd0973cc 1632 foreach ($records as $r) {
1633 $sql1 = "SELECT q.*
f34488b2 1634 FROM {question} q
1635 WHERE q.id = ?
1636 ";
fd0973cc 1637 if ( !isset ($datasetdefs["$r->type-$r->category-$r->name"])){
1638 $datasetdefs["$r->type-$r->category-$r->name"]= $r;
1639 }
f34488b2 1640 if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
fd0973cc 1641 $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question]->name =$questionb[$r->question]->name ;
1642 }
1643 }
1644 }
1645 }
1646 if (!empty ($datasetdefs)){
f34488b2 1647
1648 $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>";
fd0973cc 1649 foreach ($datasetdefs as $datasetdef){
1650 list($distribution, $min, $max,$dec) = explode(':', $datasetdef->options, 4);
1651 $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\">";
1652 foreach ($datasetdef->questions as $qu) {
1653 //limit the name length displayed
1654 if (!empty($qu->name)) {
1655 $qu->name = (strlen($qu->name) > $lnamemax) ?
1656 substr($qu->name, 0, $lnamemax).'...' : $qu->name;
1657 } else {
1658 $qu->name = '';
1659 }
f34488b2 1660 $text .=" &nbsp;&nbsp; $qu->name <br/>";
1661 }
fd0973cc 1662 $text .="</td></tr>";
1663 }
1664 $text .="</table>";
1665 }else{
f34488b2 1666 $text .=get_string('nosharedwildcard', 'qtype_calculated');
fd0973cc 1667 }
1668 return $text ;
f34488b2 1669 }
fbe2cfea 1670 function get_virtual_qtype() {
1671 if (!$this->virtualqtype) {
1672 $this->virtualqtype = $this->create_virtual_qtype();
1673 }
1674 return $this->virtualqtype;
1675 }
fd0973cc 1676
92186abc 1677
c5d94c41 1678/// BACKUP FUNCTIONS ////////////////////////////
1679
1680 /*
1681 * Backup the data in the question
1682 *
1683 * This is used in question/backuplib.php
1684 */
1685 function backup($bf,$preferences,$question,$level=6) {
f34488b2 1686 global $DB;
c5d94c41 1687 $status = true;
1688
f34488b2 1689 $calculateds = $DB->get_records("question_calculated",array("question" =>$question,"id"));
c5d94c41 1690 //If there are calculated-s
1691 if ($calculateds) {
1692 //Iterate over each calculateds
1693 foreach ($calculateds as $calculated) {
1694 $status = $status &&fwrite ($bf,start_tag("CALCULATED",$level,true));
1695 //Print calculated contents
1696 fwrite ($bf,full_tag("ANSWER",$level+1,false,$calculated->answer));
1697 fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$calculated->tolerance));
1698 fwrite ($bf,full_tag("TOLERANCETYPE",$level+1,false,$calculated->tolerancetype));
1699 fwrite ($bf,full_tag("CORRECTANSWERLENGTH",$level+1,false,$calculated->correctanswerlength));
1700 fwrite ($bf,full_tag("CORRECTANSWERFORMAT",$level+1,false,$calculated->correctanswerformat));
1701 //Now backup numerical_units
1702 $status = question_backup_numerical_units($bf,$preferences,$question,7);
1703 //Now backup required dataset definitions and items...
1704 $status = question_backup_datasets($bf,$preferences,$question,7);
1705 //End calculated data
1706 $status = $status &&fwrite ($bf,end_tag("CALCULATED",$level,true));
1707 }
1ee53ca9 1708 $calculated_options = $DB->get_records("question_calculated_options",array("questionid" => $question),"id");
1709 if ($calculated_options) {
1710 //Iterate over each calculated_option
1711 foreach ($calculated_options as $calculated_option) {
1712 $status = fwrite ($bf,start_tag("CALCULATED_OPTIONS",$level,true));
1713 //Print calculated_option contents
1714 fwrite ($bf,full_tag("SYNCHRONIZE",$level+1,false,$calculated_option->synchronize));
1715 $status = fwrite ($bf,end_tag("CALCULATED_OPTIONS",$level,true));
1716 }
1717 //Now print question_answers
1718 $status = question_backup_answers($bf,$preferences,$question);
1719 }
c5d94c41 1720 }
1721 return $status;
1722 }
315559d3 1723
1724/// RESTORE FUNCTIONS /////////////////
1725
1726 /*
1727 * Restores the data in the question
1728 *
1729 * This is used in question/restorelib.php
1730 */
1731 function restore($old_question_id,$new_question_id,$info,$restore) {
9db7dab2 1732 global $DB;
315559d3 1733
1734 $status = true;
1735
1736 //Get the calculated-s array
1737 $calculateds = $info['#']['CALCULATED'];
1738
1739 //Iterate over calculateds
1740 for($i = 0; $i < sizeof($calculateds); $i++) {
1741 $cal_info = $calculateds[$i];
1742 //traverse_xmlize($cal_info); //Debug
1743 //print_object ($GLOBALS['traverse_array']); //Debug
1744 //$GLOBALS['traverse_array']=""; //Debug
1745
1746 //Now, build the question_calculated record structure
1747 $calculated->question = $new_question_id;
1748 $calculated->answer = backup_todb($cal_info['#']['ANSWER']['0']['#']);
1749 $calculated->tolerance = backup_todb($cal_info['#']['TOLERANCE']['0']['#']);
1750 $calculated->tolerancetype = backup_todb($cal_info['#']['TOLERANCETYPE']['0']['#']);
1751 $calculated->correctanswerlength = backup_todb($cal_info['#']['CORRECTANSWERLENGTH']['0']['#']);
1752 $calculated->correctanswerformat = backup_todb($cal_info['#']['CORRECTANSWERFORMAT']['0']['#']);
1753
1754 ////We have to recode the answer field
1755 $answer = backup_getid($restore->backup_unique_code,"question_answers",$calculated->answer);
1756 if ($answer) {
1757 $calculated->answer = $answer->new_id;
1758 }
1759
1760 //The structure is equal to the db, so insert the question_calculated
9db7dab2 1761 $newid = $DB->insert_record ("question_calculated",$calculated);
315559d3 1762
1763 //Do some output
1764 if (($i+1) % 50 == 0) {
1765 if (!defined('RESTORE_SILENTLY')) {
1766 echo ".";
1767 if (($i+1) % 1000 == 0) {
1768 echo "<br />";
1769 }
1770 }
1771 backup_flush(300);
1772 }
1ee53ca9 1773 //Get the calculated_options array
1774 // need to check as old questions don't have calculated_options record
1775 if(isset($info['#']['CALCULATED_OPTIONS'])){
1776 $calculatedoptions = $info['#']['CALCULATED_OPTIONS'];
1777
1778 //Iterate over calculated_options
1779 for($i = 0; $i < sizeof($calculatedoptions); $i++) {
1780 $cal_info = $calculatedoptions[$i];
1781 //traverse_xmlize($cal_info); //Debug
1782 //print_object ($GLOBALS['traverse_array']); //Debug
1783 //$GLOBALS['traverse_array']=""; //Debug
1784
1785 //Now, build the question_calculated_options record structure
1786 $calculated_options->questionid = $new_question_id;
1787 $calculated_options->synchronize = backup_todb($cal_info['#']['SYNCHRONIZE']['0']['#']);
1788 }
1789
1790 //The structure is equal to the db, so insert the question_calculated_options
1791 $newid = $DB->insert_record ("question_calculated_options",$calculated_options);
1792
1793 //Do some output
1794 if (($i+1) % 50 == 0) {
1795 if (!defined('RESTORE_SILENTLY')) {
1796 echo ".";
1797 if (($i+1) % 1000 == 0) {
1798 echo "<br />";
1799 }
1800 }
1801 backup_flush(300);
1802 }
1803 }
315559d3 1804
1805 //Now restore numerical_units
1806 $status = question_restore_numerical_units ($old_question_id,$new_question_id,$cal_info,$restore);
1807
1808 //Now restore dataset_definitions
1809 if ($status && $newid) {
1810 $status = question_restore_dataset_definitions ($old_question_id,$new_question_id,$cal_info,$restore);
1811 }
1812
1813 if (!$newid) {
1814 $status = false;
1815 }
1816 }
1817
1818 return $status;
1819 }
0d03d6be 1820
a2155a7b 1821 /**
0d03d6be 1822 * Runs all the code required to set up and save an essay question for testing purposes.
1823 * Alternate DB table prefix may be used to facilitate data deletion.
1824 */
1825 function generate_test($name, $courseid = null) {
01f3ba13 1826 global $DB;
0d03d6be 1827 list($form, $question) = parent::generate_test($name, $courseid);
1828 $form->feedback = 1;
1829 $form->multiplier = array(1, 1);
1830 $form->shuffleanswers = 1;
1831 $form->noanswers = 1;
1832 $form->qtype ='calculated';
1833 $question->qtype ='calculated';
1834 $form->answers = array('{a} + {b}');
1835 $form->fraction = array(1);
1836 $form->tolerance = array(0.01);
1837 $form->tolerancetype = array(1);
1838 $form->correctanswerlength = array(2);
1839 $form->correctanswerformat = array(1);
1840 $form->questiontext = "What is {a} + {b}?";
1841
1842 if ($courseid) {
c9f4940e 1843 $course = $DB->get_record('course', array('id'=> $courseid));
0d03d6be 1844 }
1845
1846 $new_question = $this->save_question($question, $form, $course);
1847
1848 $dataset_form = new stdClass();
1849 $dataset_form->nextpageparam["forceregeneration"]= 1;
1850 $dataset_form->calcmin = array(1 => 1.0, 2 => 1.0);
1851 $dataset_form->calcmax = array(1 => 10.0, 2 => 10.0);
1852 $dataset_form->calclength = array(1 => 1, 2 => 1);
1853 $dataset_form->number = array(1 => 5.4 , 2 => 4.9);
1854 $dataset_form->itemid = array(1 => '' , 2 => '');
1855 $dataset_form->calcdistribution = array(1 => 'uniform', 2 => 'uniform');
1856 $dataset_form->definition = array(1 => "1-0-a",
1857 2 => "1-0-b");
1858 $dataset_form->nextpageparam = array('forceregeneration' => false);
1859 $dataset_form->addbutton = 1;
1860 $dataset_form->selectadd = 1;
1861 $dataset_form->courseid = $courseid;
1862 $dataset_form->cmid = 0;
1863 $dataset_form->id = $new_question->id;
1864 $this->save_dataset_items($new_question, $dataset_form);
1865
1866 return $new_question;
1867 }
516cf3eb 1868}
1869//// END OF CLASS ////
1870
1871//////////////////////////////////////////////////////////////////////////
1872//// INITIATION - Without this line the question type is not in use... ///
1873//////////////////////////////////////////////////////////////////////////
a2156789 1874question_register_questiontype(new question_calculated_qtype());
516cf3eb 1875
7518b645 1876function qtype_calculated_calculate_answer($formula, $individualdata,
516cf3eb 1877 $tolerance, $tolerancetype, $answerlength, $answerformat='1', $unit='') {
1878/// The return value has these properties:
1879/// ->answer the correct answer
1880/// ->min the lower bound for an acceptable response
1881/// ->max the upper bound for an accetpable response
1882
1883 /// Exchange formula variables with the correct values...
f02c6f01 1884 global $QTYPES;
fbe2cfea 1885 $answer = $QTYPES['calculated']->substitute_variables_and_eval($formula, $individualdata);
516cf3eb 1886 if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */
1887 /*** Adjust to the correct number of decimals ***/
1d9ed698 1888 if (stripos($answer,'e')>0 ){
1889 $answerlengthadd = strlen($answer)-stripos($answer,'e');
1890 }else {
1891 $answerlengthadd = 0 ;
1892 }
1893 $calculated->answer = round(floatval($answer), $answerlength+$answerlengthadd);
516cf3eb 1894
1895 if ($answerlength) {
1896 /* Try to include missing zeros at the end */
1897
6dbcacee 1898 if (preg_match('~^(.*\\.)(.*)$~', $calculated->answer, $regs)) {
516cf3eb 1899 $calculated->answer = $regs[1] . substr(
1900 $regs[2] . '00000000000000000000000000000000000000000x',
1901 0, $answerlength)
1902 . $unit;
1903 } else {
1904 $calculated->answer .=
1905 substr('.00000000000000000000000000000000000000000x',
1906 0, $answerlength + 1) . $unit;
1907 }
1908 } else {
1909 /* Attach unit */
1910 $calculated->answer .= $unit;
1911 }
1912
1913 } else if ($answer) { // Significant figures does only apply if the result is non-zero
1914
1915 // Convert to positive answer...
1916 if ($answer < 0) {
1917 $answer = -$answer;
1918 $sign = '-';
1919 } else {
1920 $sign = '';
1921 }
1922
1923 // Determine the format 0.[1-9][0-9]* for the answer...
1924 $p10 = 0;
1925 while ($answer < 1) {
1926 --$p10;
1927 $answer *= 10;
1928 }
1929 while ($answer >= 1) {
1930 ++$p10;
1931 $answer /= 10;
1932 }
1933 // ... and have the answer rounded of to the correct length
1934 $answer = round($answer, $answerlength);
1935
1936 // Have the answer written on a suitable format,
1937 // Either scientific or plain numeric
1938 if (-2 > $p10 || 4 < $p10) {
1939 // Use scientific format:
1940 $eX = 'e'.--$p10;
1941 $answer *= 10;
1942 if (1 == $answerlength) {
1943 $calculated->answer = $sign.$answer.$eX.$unit;
1944 } else {
1945 // Attach additional zeros at the end of $answer,
1946 $answer .= (1==strlen($answer) ? '.' : '')
1947 . '00000000000000000000000000000000000000000x';
1948 $calculated->answer = $sign
1949 .substr($answer, 0, $answerlength +1).$eX.$unit;
1950 }
1951 } else {
1952 // Stick to plain numeric format
1953 $answer *= "1e$p10";
1954 if (0.1 <= $answer / "1e$answerlength") {
1955 $calculated->answer = $sign.$answer.$unit;
1956 } else {
1957 // Could be an idea to add some zeros here
6dbcacee 1958 $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '')
516cf3eb 1959 . '00000000000000000000000000000000000000000x';
1960 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
1961 $calculated->answer = $sign.substr($answer, 0, $oklen).$unit;
1962 }
1963 }
1964
1965 } else {
1966 $calculated->answer = 0.0;
1967 }
1968
1969 /// Return the result
1970 return $calculated;
1971}
1972
1973
7518b645 1974function qtype_calculated_find_formula_errors($formula) {
516cf3eb 1975/// Validates the formula submitted from the question edit page.
1976/// Returns false if everything is alright.
1977/// Otherwise it constructs an error message
516cf3eb 1978 // Strip away dataset names
6dbcacee 1979 while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) {
516cf3eb 1980 $formula = str_replace($regs[0], '1', $formula);
1981 }
1982
1983 // Strip away empty space and lowercase it
1984 $formula = strtolower(str_replace(' ', '', $formula));
1985
86e85775 1986 $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */
516cf3eb 1987 $operatorornumber = "[$safeoperatorchar.0-9eE]";
1988
1ee53ca9 1989 while ( preg_match("~(^|[$safeoperatorchar,(])([a-z0-9_]*)\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)~",
516cf3eb 1990 $formula, $regs)) {
1991
1992 switch ($regs[2]) {
1993 // Simple parenthesis
1994 case '':
c9026379 1995 if ($regs[4] || strlen($regs[3])==0) {
516cf3eb 1996 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
1997 }
1998 break;
1999
2000 // Zero argument functions
2001 case 'pi':
2002 if ($regs[3]) {
2003 return get_string('functiontakesnoargs', 'quiz', $regs[2]);
2004 }
2005 break;
2006
2007 // Single argument functions (the most common case)
2008 case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
2009 case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
2010 case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
2011 case 'exp': case 'expm1': case 'floor': case 'is_finite':
2012 case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
2013 case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
2014 case 'tan': case 'tanh':
2015 if ($regs[4] || empty($regs[3])) {
2016 return get_string('functiontakesonearg','quiz',$regs[2]);
2017 }
2018 break;
2019
2020 // Functions that take one or two arguments
2021 case 'log': case 'round':
2022 if ($regs[5] || empty($regs[3])) {
2023 return get_string('functiontakesoneortwoargs','quiz',$regs[2]);
2024 }
2025 break;
2026
2027 // Functions that must have two arguments
2028 case 'atan2': case 'fmod': case 'pow':
2029 if ($regs[5] || empty($regs[4])) {
2030 return get_string('functiontakestwoargs', 'quiz', $regs[2]);
2031 }
2032 break;
2033
2034 // Functions that take two or more arguments
2035 case 'min': case 'max':
2036 if (empty($regs[4])) {
2037 return get_string('functiontakesatleasttwo','quiz',$regs[2]);
2038 }
2039 break;
2040
2041 default:
2042 return get_string('unsupportedformulafunction','quiz',$regs[2]);
2043 }
2044
2045 // Exchange the function call with '1' and then chack for
2046 // another function call...
2047 if ($regs[1]) {
2048 // The function call is proceeded by an operator
2049 $formula = str_replace($regs[0], $regs[1] . '1', $formula);
2050 } else {
2051 // The function call starts the formula
6dbcacee 2052 $formula = preg_replace("~^$regs[2]\\([^)]*\\)~", '1', $formula);
516cf3eb 2053 }
2054 }
2055
6dbcacee 2056 if (preg_match("~[^$safeoperatorchar.0-9eE]+~", $formula, $regs)) {
516cf3eb 2057 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
2058 } else {
2059 // Formula just might be valid
2060 return false;
2061 }
fd6b864f 2062
516cf3eb 2063}
2064
2065function dump($obj) {
2066 echo "<pre>\n";
2067 var_dump($obj);
2068 echo "</pre><br />\n";
2069}
2070
2071?>