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