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