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