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