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