|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 // This file is part of Moodle - http://moodle.org/ 00003 // 00004 // Moodle is free software: you can redistribute it and/or modify 00005 // it under the terms of the GNU General Public License as published by 00006 // the Free Software Foundation, either version 3 of the License, or 00007 // (at your option) any later version. 00008 // 00009 // Moodle is distributed in the hope that it will be useful, 00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 // GNU General Public License for more details. 00013 // 00014 // You should have received a copy of the GNU General Public License 00015 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00016 00027 defined('MOODLE_INTERNAL') || die(); 00028 00029 require_once($CFG->dirroot . '/question/type/numerical/question.php'); 00030 00031 00038 class qtype_calculated extends question_type { 00039 const MAX_DATASET_ITEMS = 100; 00040 00041 public $wizardpagesnumber = 3; 00042 00043 public function get_question_options($question) { 00044 // First get the datasets and default options 00045 // the code is used for calculated, calculatedsimple and calculatedmulti qtypes 00046 global $CFG, $DB, $OUTPUT; 00047 if (!$question->options = $DB->get_record('question_calculated_options', 00048 array('question' => $question->id))) { 00049 $question->options->synchronize = 0; 00050 $question->options->single = 0; 00051 $question->options->answernumbering = 'abc'; 00052 $question->options->shuffleanswers = 0; 00053 $question->options->correctfeedback = ''; 00054 $question->options->partiallycorrectfeedback = ''; 00055 $question->options->incorrectfeedback = ''; 00056 $question->options->correctfeedbackformat = 0; 00057 $question->options->partiallycorrectfeedbackformat = 0; 00058 $question->options->incorrectfeedbackformat = 0; 00059 } 00060 00061 if (!$question->options->answers = $DB->get_records_sql(" 00062 SELECT a.*, c.tolerance, c.tolerancetype, c.correctanswerlength, c.correctanswerformat 00063 FROM {question_answers} a, 00064 {question_calculated} c 00065 WHERE a.question = ? 00066 AND a.id = c.answer 00067 ORDER BY a.id ASC", array($question->id))) { 00068 return false; 00069 } 00070 00071 if ($this->get_virtual_qtype()->name() == 'numerical') { 00072 $this->get_virtual_qtype()->get_numerical_units($question); 00073 $this->get_virtual_qtype()->get_numerical_options($question); 00074 } 00075 00076 $question->hints = $DB->get_records('question_hints', 00077 array('questionid' => $question->id), 'id ASC'); 00078 00079 if (isset($question->export_process)&&$question->export_process) { 00080 $question->options->datasets = $this->get_datasets_for_export($question); 00081 } 00082 return true; 00083 } 00084 00085 public function get_datasets_for_export($question) { 00086 global $DB, $CFG; 00087 $datasetdefs = array(); 00088 if (!empty($question->id)) { 00089 $sql = "SELECT i.* 00090 FROM {question_datasets} d, {question_dataset_definitions} i 00091 WHERE d.question = ? AND d.datasetdefinition = i.id"; 00092 if ($records = $DB->get_records_sql($sql, array($question->id))) { 00093 foreach ($records as $r) { 00094 $def = $r; 00095 if ($def->category == '0') { 00096 $def->status = 'private'; 00097 } else { 00098 $def->status = 'shared'; 00099 } 00100 $def->type = 'calculated'; 00101 list($distribution, $min, $max, $dec) = explode(':', $def->options, 4); 00102 $def->distribution = $distribution; 00103 $def->minimum = $min; 00104 $def->maximum = $max; 00105 $def->decimals = $dec; 00106 if ($def->itemcount > 0) { 00107 // get the datasetitems 00108 $def->items = array(); 00109 if ($items = $this->get_database_dataset_items($def->id)) { 00110 $n = 0; 00111 foreach ($items as $ii) { 00112 $n++; 00113 $def->items[$n] = new stdClass(); 00114 $def->items[$n]->itemnumber = $ii->itemnumber; 00115 $def->items[$n]->value = $ii->value; 00116 } 00117 $def->number_of_items = $n; 00118 } 00119 } 00120 $datasetdefs["1-$r->category-$r->name"] = $def; 00121 } 00122 } 00123 } 00124 return $datasetdefs; 00125 } 00126 00127 public function save_question_options($question) { 00128 global $CFG, $DB; 00129 // the code is used for calculated, calculatedsimple and calculatedmulti qtypes 00130 $context = $question->context; 00131 if (isset($question->answer) && !isset($question->answers)) { 00132 $question->answers = $question->answer; 00133 } 00134 // calculated options 00135 $update = true; 00136 $options = $DB->get_record('question_calculated_options', 00137 array('question' => $question->id)); 00138 if (!$options) { 00139 $update = false; 00140 $options = new stdClass(); 00141 $options->question = $question->id; 00142 } 00143 // as used only by calculated 00144 if (isset($question->synchronize)) { 00145 $options->synchronize = $question->synchronize; 00146 } else { 00147 $options->synchronize = 0; 00148 } 00149 $options->single = 0; 00150 $options->answernumbering = $question->answernumbering; 00151 $options->shuffleanswers = $question->shuffleanswers; 00152 00153 foreach (array('correctfeedback', 'partiallycorrectfeedback', 00154 'incorrectfeedback') as $feedbackname) { 00155 $options->$feedbackname = ''; 00156 $feedbackformat = $feedbackname . 'format'; 00157 $options->$feedbackformat = 0; 00158 } 00159 00160 if ($update) { 00161 $DB->update_record('question_calculated_options', $options); 00162 } else { 00163 $DB->insert_record('question_calculated_options', $options); 00164 } 00165 00166 // Get old versions of the objects 00167 $oldanswers = $DB->get_records('question_answers', 00168 array('question' => $question->id), 'id ASC'); 00169 00170 $oldoptions = $DB->get_records('question_calculated', 00171 array('question' => $question->id), 'answer ASC'); 00172 00173 // Save the units. 00174 $virtualqtype = $this->get_virtual_qtype(); 00175 00176 $result = $virtualqtype->save_units($question); 00177 if (isset($result->error)) { 00178 return $result; 00179 } else { 00180 $units = $result->units; 00181 } 00182 00183 // Insert all the new answers 00184 if (isset($question->answer) && !isset($question->answers)) { 00185 $question->answers = $question->answer; 00186 } 00187 foreach ($question->answers as $key => $answerdata) { 00188 if (is_array($answerdata)) { 00189 $answerdata = $answerdata['text']; 00190 } 00191 if (trim($answerdata) == '') { 00192 continue; 00193 } 00194 00195 // Update an existing answer if possible. 00196 $answer = array_shift($oldanswers); 00197 if (!$answer) { 00198 $answer = new stdClass(); 00199 $answer->question = $question->id; 00200 $answer->answer = ''; 00201 $answer->feedback = ''; 00202 $answer->id = $DB->insert_record('question_answers', $answer); 00203 } 00204 00205 $answer->answer = trim($answerdata); 00206 $answer->fraction = $question->fraction[$key]; 00207 $answer->feedback = $this->import_or_save_files($question->feedback[$key], 00208 $context, 'question', 'answerfeedback', $answer->id); 00209 $answer->feedbackformat = $question->feedback[$key]['format']; 00210 00211 $DB->update_record("question_answers", $answer); 00212 00213 // Set up the options object 00214 if (!$options = array_shift($oldoptions)) { 00215 $options = new stdClass(); 00216 } 00217 $options->question = $question->id; 00218 $options->answer = $answer->id; 00219 $options->tolerance = trim($question->tolerance[$key]); 00220 $options->tolerancetype = trim($question->tolerancetype[$key]); 00221 $options->correctanswerlength = trim($question->correctanswerlength[$key]); 00222 $options->correctanswerformat = trim($question->correctanswerformat[$key]); 00223 00224 // Save options 00225 if (isset($options->id)) { 00226 // reusing existing record 00227 $DB->update_record('question_calculated', $options); 00228 } else { 00229 // new options 00230 $DB->insert_record('question_calculated', $options); 00231 } 00232 } 00233 00234 // delete old answer records 00235 if (!empty($oldanswers)) { 00236 foreach ($oldanswers as $oa) { 00237 $DB->delete_records('question_answers', array('id' => $oa->id)); 00238 } 00239 } 00240 00241 // delete old answer records 00242 if (!empty($oldoptions)) { 00243 foreach ($oldoptions as $oo) { 00244 $DB->delete_records('question_calculated', array('id' => $oo->id)); 00245 } 00246 } 00247 00248 $result = $virtualqtype->save_unit_options($question); 00249 if (isset($result->error)) { 00250 return $result; 00251 } 00252 00253 $this->save_hints($question); 00254 00255 if (isset($question->import_process)&&$question->import_process) { 00256 $this->import_datasets($question); 00257 } 00258 // Report any problems. 00259 if (!empty($result->notice)) { 00260 return $result; 00261 } 00262 return true; 00263 } 00264 00265 public function import_datasets($question) { 00266 global $DB; 00267 $n = count($question->dataset); 00268 foreach ($question->dataset as $dataset) { 00269 // name, type, option, 00270 $datasetdef = new stdClass(); 00271 $datasetdef->name = $dataset->name; 00272 $datasetdef->type = 1; 00273 $datasetdef->options = $dataset->distribution . ':' . $dataset->min . ':' . 00274 $dataset->max . ':' . $dataset->length; 00275 $datasetdef->itemcount = $dataset->itemcount; 00276 if ($dataset->status == 'private') { 00277 $datasetdef->category = 0; 00278 $todo = 'create'; 00279 } else if ($dataset->status == 'shared') { 00280 if ($sharedatasetdefs = $DB->get_records_select( 00281 'question_dataset_definitions', 00282 "type = '1' 00283 AND name = ? 00284 AND category = ? 00285 ORDER BY id DESC ", array($dataset->name, $question->category) 00286 )) { // so there is at least one 00287 $sharedatasetdef = array_shift($sharedatasetdefs); 00288 if ($sharedatasetdef->options == $datasetdef->options) {// identical so use it 00289 $todo = 'useit'; 00290 $datasetdef = $sharedatasetdef; 00291 } else { // different so create a private one 00292 $datasetdef->category = 0; 00293 $todo = 'create'; 00294 } 00295 } else { // no so create one 00296 $datasetdef->category = $question->category; 00297 $todo = 'create'; 00298 } 00299 } 00300 if ($todo == 'create') { 00301 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef); 00302 } 00303 // Create relation to the dataset: 00304 $questiondataset = new stdClass(); 00305 $questiondataset->question = $question->id; 00306 $questiondataset->datasetdefinition = $datasetdef->id; 00307 $DB->insert_record('question_datasets', $questiondataset); 00308 if ($todo == 'create') { 00309 // add the items 00310 foreach ($dataset->datasetitem as $dataitem) { 00311 $datasetitem = new stdClass(); 00312 $datasetitem->definition = $datasetdef->id; 00313 $datasetitem->itemnumber = $dataitem->itemnumber; 00314 $datasetitem->value = $dataitem->value; 00315 $DB->insert_record('question_dataset_items', $datasetitem); 00316 } 00317 } 00318 } 00319 } 00320 00321 protected function initialise_question_instance(question_definition $question, $questiondata) { 00322 parent::initialise_question_instance($question, $questiondata); 00323 00324 question_bank::get_qtype('numerical')->initialise_numerical_answers( 00325 $question, $questiondata); 00326 foreach ($questiondata->options->answers as $a) { 00327 $question->answers[$a->id]->tolerancetype = $a->tolerancetype; 00328 $question->answers[$a->id]->correctanswerlength = $a->correctanswerlength; 00329 $question->answers[$a->id]->correctanswerformat = $a->correctanswerformat; 00330 } 00331 00332 $question->synchronised = $questiondata->options->synchronize; 00333 00334 $question->unitdisplay = $questiondata->options->showunits; 00335 $question->unitgradingtype = $questiondata->options->unitgradingtype; 00336 $question->unitpenalty = $questiondata->options->unitpenalty; 00337 $question->ap = question_bank::get_qtype( 00338 'numerical')->make_answer_processor( 00339 $questiondata->options->units, $questiondata->options->unitsleft); 00340 00341 $question->datasetloader = new qtype_calculated_dataset_loader($questiondata->id); 00342 } 00343 00344 public function validate_form($form) { 00345 switch($form->wizardpage) { 00346 case 'question': 00347 $calculatedmessages = array(); 00348 if (empty($form->name)) { 00349 $calculatedmessages[] = get_string('missingname', 'qtype_calculated'); 00350 } 00351 if (empty($form->questiontext)) { 00352 $calculatedmessages[] = get_string('missingquestiontext', 'qtype_calculated'); 00353 } 00354 // Verify formulas 00355 foreach ($form->answers as $key => $answer) { 00356 if ('' === trim($answer)) { 00357 $calculatedmessages[] = get_string( 00358 'missingformula', 'qtype_calculated'); 00359 } 00360 if ($formulaerrors = qtype_calculated_find_formula_errors($answer)) { 00361 $calculatedmessages[] = $formulaerrors; 00362 } 00363 if (! isset($form->tolerance[$key])) { 00364 $form->tolerance[$key] = 0.0; 00365 } 00366 if (! is_numeric($form->tolerance[$key])) { 00367 $calculatedmessages[] = get_string( 00368 'tolerancemustbenumeric', 'qtype_calculated'); 00369 } 00370 } 00371 00372 if (!empty($calculatedmessages)) { 00373 $errorstring = "The following errors were found:<br />"; 00374 foreach ($calculatedmessages as $msg) { 00375 $errorstring .= $msg . '<br />'; 00376 } 00377 print_error($errorstring); 00378 } 00379 00380 break; 00381 default: 00382 return parent::validate_form($form); 00383 break; 00384 } 00385 return true; 00386 } 00387 public function finished_edit_wizard($form) { 00388 return isset($form->savechanges); 00389 } 00390 public function wizardpagesnumber() { 00391 return 3; 00392 } 00393 // This gets called by editquestion.php after the standard question is saved 00394 public function print_next_wizard_page($question, $form, $course) { 00395 global $CFG, $SESSION, $COURSE; 00396 00397 // Catch invalid navigation & reloads 00398 if (empty($question->id) && empty($SESSION->calculated)) { 00399 redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired.', 3); 00400 } 00401 00402 // See where we're coming from 00403 switch($form->wizardpage) { 00404 case 'question': 00405 require("$CFG->dirroot/question/type/calculated/datasetdefinitions.php"); 00406 break; 00407 case 'datasetdefinitions': 00408 case 'datasetitems': 00409 require("$CFG->dirroot/question/type/calculated/datasetitems.php"); 00410 break; 00411 default: 00412 print_error('invalidwizardpage', 'question'); 00413 break; 00414 } 00415 } 00416 00417 // This gets called by question2.php after the standard question is saved 00418 public function &next_wizard_form($submiturl, $question, $wizardnow) { 00419 global $CFG, $SESSION, $COURSE; 00420 00421 // Catch invalid navigation & reloads 00422 if (empty($question->id) && empty($SESSION->calculated)) { 00423 redirect('edit.php?courseid=' . $COURSE->id, 00424 'The page you are loading has expired. Cannot get next wizard form.', 3); 00425 } 00426 if (empty($question->id)) { 00427 $question = $SESSION->calculated->questionform; 00428 } 00429 00430 // See where we're coming from 00431 switch($wizardnow) { 00432 case 'datasetdefinitions': 00433 require("$CFG->dirroot/question/type/calculated/datasetdefinitions_form.php"); 00434 $mform = new question_dataset_dependent_definitions_form( 00435 "$submiturl?wizardnow=datasetdefinitions", $question); 00436 break; 00437 case 'datasetitems': 00438 require("$CFG->dirroot/question/type/calculated/datasetitems_form.php"); 00439 $regenerate = optional_param('forceregeneration', false, PARAM_BOOL); 00440 $mform = new question_dataset_dependent_items_form( 00441 "$submiturl?wizardnow=datasetitems", $question, $regenerate); 00442 break; 00443 default: 00444 print_error('invalidwizardpage', 'question'); 00445 break; 00446 } 00447 00448 return $mform; 00449 } 00450 00459 public function display_question_editing_page($mform, $question, $wizardnow) { 00460 global $OUTPUT; 00461 switch ($wizardnow) { 00462 case '': 00463 // On the first page, the default display is fine. 00464 parent::display_question_editing_page($mform, $question, $wizardnow); 00465 return; 00466 00467 case 'datasetdefinitions': 00468 echo $OUTPUT->heading_with_help( 00469 get_string('choosedatasetproperties', 'qtype_calculated'), 00470 'questiondatasets', 'qtype_calculated'); 00471 break; 00472 00473 case 'datasetitems': 00474 echo $OUTPUT->heading_with_help(get_string('editdatasets', 'qtype_calculated'), 00475 'questiondatasets', 'qtype_calculated'); 00476 break; 00477 } 00478 00479 $mform->display(); 00480 } 00481 00494 public function preparedatasets($form , $questionfromid = '0') { 00495 // the dataset names present in the edit_question_form and edit_calculated_form 00496 // are retrieved 00497 $possibledatasets = $this->find_dataset_names($form->questiontext); 00498 $mandatorydatasets = array(); 00499 foreach ($form->answers as $answer) { 00500 $mandatorydatasets += $this->find_dataset_names($answer); 00501 } 00502 // if there are identical datasetdefs already saved in the original question. 00503 // either when editing a question or saving as new 00504 // they are retrieved using $questionfromid 00505 if ($questionfromid != '0') { 00506 $form->id = $questionfromid; 00507 } 00508 $datasets = array(); 00509 $key = 0; 00510 // always prepare the mandatorydatasets present in the answers 00511 // the $options are not used here 00512 foreach ($mandatorydatasets as $datasetname) { 00513 if (!isset($datasets[$datasetname])) { 00514 list($options, $selected) = 00515 $this->dataset_options($form, $datasetname); 00516 $datasets[$datasetname] = ''; 00517 $form->dataset[$key] = $selected; 00518 $key++; 00519 } 00520 } 00521 // do not prepare possibledatasets when creating a question 00522 // they will defined and stored with datasetdefinitions_form.php 00523 // the $options are not used here 00524 if ($questionfromid != '0') { 00525 00526 foreach ($possibledatasets as $datasetname) { 00527 if (!isset($datasets[$datasetname])) { 00528 list($options, $selected) = 00529 $this->dataset_options($form, $datasetname, false); 00530 $datasets[$datasetname] = ''; 00531 $form->dataset[$key] = $selected; 00532 $key++; 00533 } 00534 } 00535 } 00536 return $datasets; 00537 } 00538 public function addnamecategory(&$question) { 00539 global $DB; 00540 $categorydatasetdefs = $DB->get_records_sql( 00541 "SELECT a.* 00542 FROM {question_datasets} b, {question_dataset_definitions} a 00543 WHERE a.id = b.datasetdefinition 00544 AND a.type = '1' 00545 AND a.category != 0 00546 AND b.question = ? 00547 ORDER BY a.name ", array($question->id)); 00548 $questionname = $question->name; 00549 $regs= array(); 00550 if (preg_match('~#\{([^[:space:]]*)#~', $questionname , $regs)) { 00551 $questionname = str_replace($regs[0], '', $questionname); 00552 }; 00553 00554 if (!empty($categorydatasetdefs)) { 00555 // there is at least one with the same name 00556 $questionname = '#' . $questionname; 00557 foreach ($categorydatasetdefs as $def) { 00558 if (strlen($def->name) + strlen($questionname) < 250) { 00559 $questionname = '{' . $def->name . '}' . $questionname; 00560 } 00561 } 00562 $questionname = '#' . $questionname; 00563 } 00564 $DB->set_field('question', 'name', $questionname, array('id' => $question->id)); 00565 } 00566 00586 public function save_question($question, $form) { 00587 global $DB; 00588 if ($this->wizardpagesnumber() == 1) { 00589 $question = parent::save_question($question, $form); 00590 return $question; 00591 } 00592 00593 $wizardnow = optional_param('wizardnow', '', PARAM_ALPHA); 00594 $id = optional_param('id', 0, PARAM_INT); // question id 00595 // in case 'question' 00596 // for a new question $form->id is empty 00597 // when saving as new question 00598 // $question->id = 0, $form is $data from question2.php 00599 // and $data->makecopy is defined as $data->id is the initial question id 00600 // edit case. If it is a new question we don't necessarily need to 00601 // return a valid question object 00602 00603 // See where we're coming from 00604 switch($wizardnow) { 00605 case '' : 00606 case 'question': // coming from the first page, creating the second 00607 if (empty($form->id)) { // for a new question $form->id is empty 00608 $question = parent::save_question($question, $form); 00609 //prepare the datasets using default $questionfromid 00610 $this->preparedatasets($form); 00611 $form->id = $question->id; 00612 $this->save_dataset_definitions($form); 00613 if (isset($form->synchronize) && $form->synchronize == 2) { 00614 $this->addnamecategory($question); 00615 } 00616 } else if (!empty($form->makecopy)) { 00617 $questionfromid = $form->id; 00618 $question = parent::save_question($question, $form); 00619 //prepare the datasets 00620 $this->preparedatasets($form, $questionfromid); 00621 $form->id = $question->id; 00622 $this->save_as_new_dataset_definitions($form, $questionfromid); 00623 if (isset($form->synchronize) && $form->synchronize == 2) { 00624 $this->addnamecategory($question); 00625 } 00626 } else { 00627 // editing a question 00628 $question = parent::save_question($question, $form); 00629 //prepare the datasets 00630 $this->preparedatasets($form, $question->id); 00631 $form->id = $question->id; 00632 $this->save_dataset_definitions($form); 00633 if (isset($form->synchronize) && $form->synchronize == 2) { 00634 $this->addnamecategory($question); 00635 } 00636 } 00637 break; 00638 case 'datasetdefinitions': 00639 // calculated options 00640 // it cannot go here without having done the first page 00641 // so the question_calculated_options should exist 00642 // only need to update the synchronize field 00643 if (isset($form->synchronize)) { 00644 $optionssynchronize = $form->synchronize; 00645 } else { 00646 $optionssynchronize = 0; 00647 } 00648 $DB->set_field('question_calculated_options', 'synchronize', $optionssynchronize, 00649 array('question' => $question->id)); 00650 if (isset($form->synchronize) && $form->synchronize == 2) { 00651 $this->addnamecategory($question); 00652 } 00653 00654 $this->save_dataset_definitions($form); 00655 break; 00656 case 'datasetitems': 00657 $this->save_dataset_items($question, $form); 00658 $this->save_question_calculated($question, $form); 00659 break; 00660 default: 00661 print_error('invalidwizardpage', 'question'); 00662 break; 00663 } 00664 return $question; 00665 } 00666 00667 public function delete_question($questionid, $contextid) { 00668 global $DB; 00669 00670 $DB->delete_records('question_calculated', array('question' => $questionid)); 00671 $DB->delete_records('question_calculated_options', array('question' => $questionid)); 00672 $DB->delete_records('question_numerical_units', array('question' => $questionid)); 00673 if ($datasets = $DB->get_records('question_datasets', array('question' => $questionid))) { 00674 foreach ($datasets as $dataset) { 00675 if (!$DB->get_records_select('question_datasets', 00676 "question != ? AND datasetdefinition = ? ", 00677 array($questionid, $dataset->datasetdefinition))) { 00678 $DB->delete_records('question_dataset_definitions', 00679 array('id' => $dataset->datasetdefinition)); 00680 $DB->delete_records('question_dataset_items', 00681 array('definition' => $dataset->datasetdefinition)); 00682 } 00683 } 00684 } 00685 $DB->delete_records('question_datasets', array('question' => $questionid)); 00686 00687 parent::delete_question($questionid, $contextid); 00688 } 00689 00690 public function get_random_guess_score($questiondata) { 00691 foreach ($questiondata->options->answers as $aid => $answer) { 00692 if ('*' == trim($answer->answer)) { 00693 return max($answer->fraction - $questiondata->options->unitpenalty, 0); 00694 } 00695 } 00696 return 0; 00697 } 00698 00699 public function supports_dataset_item_generation() { 00700 // Calcualted support generation of randomly distributed number data 00701 return true; 00702 } 00703 00704 public function custom_generator_tools_part($mform, $idx, $j) { 00705 00706 $minmaxgrp = array(); 00707 $minmaxgrp[] = $mform->createElement('text', "calcmin[$idx]", 00708 get_string('calcmin', 'qtype_calculated')); 00709 $minmaxgrp[] = $mform->createElement('text', "calcmax[$idx]", 00710 get_string('calcmax', 'qtype_calculated')); 00711 $mform->addGroup($minmaxgrp, 'minmaxgrp', 00712 get_string('minmax', 'qtype_calculated'), ' - ', false); 00713 $mform->setType("calcmin[$idx]", PARAM_NUMBER); 00714 $mform->setType("calcmax[$idx]", PARAM_NUMBER); 00715 00716 $precisionoptions = range(0, 10); 00717 $mform->addElement('select', "calclength[$idx]", 00718 get_string('calclength', 'qtype_calculated'), $precisionoptions); 00719 00720 $distriboptions = array('uniform' => get_string('uniform', 'qtype_calculated'), 00721 'loguniform' => get_string('loguniform', 'qtype_calculated')); 00722 $mform->addElement('select', "calcdistribution[$idx]", 00723 get_string('calcdistribution', 'qtype_calculated'), $distriboptions); 00724 } 00725 00726 public function custom_generator_set_data($datasetdefs, $formdata) { 00727 $idx = 1; 00728 foreach ($datasetdefs as $datasetdef) { 00729 if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~', 00730 $datasetdef->options, $regs)) { 00731 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name"; 00732 $formdata["calcdistribution[$idx]"] = $regs[1]; 00733 $formdata["calcmin[$idx]"] = $regs[2]; 00734 $formdata["calcmax[$idx]"] = $regs[3]; 00735 $formdata["calclength[$idx]"] = $regs[4]; 00736 } 00737 $idx++; 00738 } 00739 return $formdata; 00740 } 00741 00742 public function custom_generator_tools($datasetdef) { 00743 global $OUTPUT; 00744 if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~', 00745 $datasetdef->options, $regs)) { 00746 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name"; 00747 for ($i = 0; $i<10; ++$i) { 00748 $lengthoptions[$i] = get_string(($regs[1] == 'uniform' 00749 ? 'decimals' 00750 : 'significantfigures'), 'qtype_calculated', $i); 00751 } 00752 $menu1 = html_writer::select($lengthoptions, 'calclength[]', $regs[4], null); 00753 00754 $options = array('uniform' => get_string('uniformbit', 'qtype_calculated'), 00755 'loguniform' => get_string('loguniformbit', 'qtype_calculated')); 00756 $menu2 = html_writer::select($options, 'calcdistribution[]', $regs[1], null); 00757 return '<input type="submit" onclick="' 00758 . "getElementById('addform').regenerateddefid.value='$defid'; return true;" 00759 .'" value="'. get_string('generatevalue', 'qtype_calculated') . '"/><br/>' 00760 . '<input type="text" size="3" name="calcmin[]" ' 00761 . " value=\"$regs[2]\"/> & <input name=\"calcmax[]\" " 00762 . ' type="text" size="3" value="' . $regs[3] .'"/> ' 00763 . $menu1 . '<br/>' 00764 . $menu2; 00765 } else { 00766 return ''; 00767 } 00768 } 00769 00770 00771 public function update_dataset_options($datasetdefs, $form) { 00772 global $OUTPUT; 00773 // Do we have information about new options??? 00774 if (empty($form->definition) || empty($form->calcmin) 00775 || empty($form->calcmax) || empty($form->calclength) 00776 || empty($form->calcdistribution)) { 00777 // I guess not 00778 00779 } else { 00780 // Looks like we just could have some new information here 00781 $uniquedefs = array_values(array_unique($form->definition)); 00782 foreach ($uniquedefs as $key => $defid) { 00783 if (isset($datasetdefs[$defid]) 00784 && is_numeric($form->calcmin[$key+1]) 00785 && is_numeric($form->calcmax[$key+1]) 00786 && is_numeric($form->calclength[$key+1])) { 00787 switch ($form->calcdistribution[$key+1]) { 00788 case 'uniform': case 'loguniform': 00789 $datasetdefs[$defid]->options = 00790 $form->calcdistribution[$key+1] . ':' 00791 . $form->calcmin[$key+1] . ':' 00792 . $form->calcmax[$key+1] . ':' 00793 . $form->calclength[$key+1]; 00794 break; 00795 default: 00796 echo $OUTPUT->notification( 00797 "Unexpected distribution ".$form->calcdistribution[$key+1]); 00798 } 00799 } 00800 } 00801 } 00802 00803 // Look for empty options, on which we set default values 00804 foreach ($datasetdefs as $defid => $def) { 00805 if (empty($def->options)) { 00806 $datasetdefs[$defid]->options = 'uniform:1.0:10.0:1'; 00807 } 00808 } 00809 return $datasetdefs; 00810 } 00811 00812 public function save_question_calculated($question, $fromform) { 00813 global $DB; 00814 00815 foreach ($question->options->answers as $key => $answer) { 00816 if ($options = $DB->get_record('question_calculated', array('answer' => $key))) { 00817 $options->tolerance = trim($fromform->tolerance[$key]); 00818 $options->tolerancetype = trim($fromform->tolerancetype[$key]); 00819 $options->correctanswerlength = trim($fromform->correctanswerlength[$key]); 00820 $options->correctanswerformat = trim($fromform->correctanswerformat[$key]); 00821 $DB->update_record('question_calculated', $options); 00822 } 00823 } 00824 } 00825 00833 public function get_database_dataset_items($definition) { 00834 global $CFG, $DB; 00835 $databasedataitems = $DB->get_records_sql(// Use number as key!! 00836 " SELECT id , itemnumber, definition, value 00837 FROM {question_dataset_items} 00838 WHERE definition = $definition order by id DESC ", array($definition)); 00839 $dataitems = Array(); 00840 foreach ($databasedataitems as $id => $dataitem) { 00841 if (!isset($dataitems[$dataitem->itemnumber])) { 00842 $dataitems[$dataitem->itemnumber] = $dataitem; 00843 } 00844 } 00845 ksort($dataitems); 00846 return $dataitems; 00847 } 00848 00849 public function save_dataset_items($question, $fromform) { 00850 global $CFG, $DB; 00851 $synchronize = false; 00852 if (isset($fromform->nextpageparam['forceregeneration'])) { 00853 $regenerate = $fromform->nextpageparam['forceregeneration']; 00854 } else { 00855 $regenerate = 0; 00856 } 00857 if (empty($question->options)) { 00858 $this->get_question_options($question); 00859 } 00860 if (!empty($question->options->synchronize)) { 00861 $synchronize = true; 00862 } 00863 00864 //get the old datasets for this question 00865 $datasetdefs = $this->get_dataset_definitions($question->id, array()); 00866 // Handle generator options... 00867 $olddatasetdefs = fullclone($datasetdefs); 00868 $datasetdefs = $this->update_dataset_options($datasetdefs, $fromform); 00869 $maxnumber = -1; 00870 foreach ($datasetdefs as $defid => $datasetdef) { 00871 if (isset($datasetdef->id) 00872 && $datasetdef->options != $olddatasetdefs[$defid]->options) { 00873 // Save the new value for options 00874 $DB->update_record('question_dataset_definitions', $datasetdef); 00875 00876 } 00877 // Get maxnumber 00878 if ($maxnumber == -1 || $datasetdef->itemcount < $maxnumber) { 00879 $maxnumber = $datasetdef->itemcount; 00880 } 00881 } 00882 // Handle adding and removing of dataset items 00883 $i = 1; 00884 if ($maxnumber > self::MAX_DATASET_ITEMS) { 00885 $maxnumber = self::MAX_DATASET_ITEMS; 00886 } 00887 00888 ksort($fromform->definition); 00889 foreach ($fromform->definition as $key => $defid) { 00890 //if the delete button has not been pressed then skip the datasetitems 00891 //in the 'add item' part of the form. 00892 if ($i > count($datasetdefs)*$maxnumber) { 00893 break; 00894 } 00895 $addeditem = new stdClass(); 00896 $addeditem->definition = $datasetdefs[$defid]->id; 00897 $addeditem->value = $fromform->number[$i]; 00898 $addeditem->itemnumber = ceil($i / count($datasetdefs)); 00899 00900 if ($fromform->itemid[$i]) { 00901 // Reuse any previously used record 00902 $addeditem->id = $fromform->itemid[$i]; 00903 $DB->update_record('question_dataset_items', $addeditem); 00904 } else { 00905 $DB->insert_record('question_dataset_items', $addeditem); 00906 } 00907 00908 $i++; 00909 } 00910 if (isset($addeditem->itemnumber) && $maxnumber < $addeditem->itemnumber 00911 && $addeditem->itemnumber < self::MAX_DATASET_ITEMS) { 00912 $maxnumber = $addeditem->itemnumber; 00913 foreach ($datasetdefs as $key => $newdef) { 00914 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) { 00915 $newdef->itemcount = $maxnumber; 00916 // Save the new value for options 00917 $DB->update_record('question_dataset_definitions', $newdef); 00918 } 00919 } 00920 } 00921 // adding supplementary items 00922 $numbertoadd = 0; 00923 if (isset($fromform->addbutton) && $fromform->selectadd > 0 && 00924 $maxnumber < self::MAX_DATASET_ITEMS) { 00925 $numbertoadd = $fromform->selectadd; 00926 if (self::MAX_DATASET_ITEMS - $maxnumber < $numbertoadd) { 00927 $numbertoadd = self::MAX_DATASET_ITEMS - $maxnumber; 00928 } 00929 //add the other items. 00930 // Generate a new dataset item (or reuse an old one) 00931 foreach ($datasetdefs as $defid => $datasetdef) { 00932 // in case that for category datasets some new items has been added 00933 // get actual values 00934 // fix regenerate for this datadefs 00935 $defregenerate = 0; 00936 if ($synchronize && 00937 !empty ($fromform->nextpageparam["datasetregenerate[$datasetdef->name"])) { 00938 $defregenerate = 1; 00939 } else if (!$synchronize && 00940 (($regenerate == 1 && $datasetdef->category == 0) ||$regenerate == 2)) { 00941 $defregenerate = 1; 00942 } 00943 if (isset($datasetdef->id)) { 00944 $datasetdefs[$defid]->items = 00945 $this->get_database_dataset_items($datasetdef->id); 00946 } 00947 for ($numberadded = $maxnumber+1; $numberadded <= $maxnumber + $numbertoadd; 00948 $numberadded++) { 00949 if (isset($datasetdefs[$defid]->items[$numberadded])) { 00950 // in case of regenerate it modifies the already existing record 00951 if ($defregenerate) { 00952 $datasetitem = new stdClass(); 00953 $datasetitem->id = $datasetdefs[$defid]->items[$numberadded]->id; 00954 $datasetitem->definition = $datasetdef->id; 00955 $datasetitem->itemnumber = $numberadded; 00956 $datasetitem->value = 00957 $this->generate_dataset_item($datasetdef->options); 00958 $DB->update_record('question_dataset_items', $datasetitem); 00959 } 00960 //if not regenerate do nothing as there is already a record 00961 } else { 00962 $datasetitem = new stdClass(); 00963 $datasetitem->definition = $datasetdef->id; 00964 $datasetitem->itemnumber = $numberadded; 00965 if ($this->supports_dataset_item_generation()) { 00966 $datasetitem->value = 00967 $this->generate_dataset_item($datasetdef->options); 00968 } else { 00969 $datasetitem->value = ''; 00970 } 00971 $DB->insert_record('question_dataset_items', $datasetitem); 00972 } 00973 }//for number added 00974 }// datasetsdefs end 00975 $maxnumber += $numbertoadd; 00976 foreach ($datasetdefs as $key => $newdef) { 00977 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) { 00978 $newdef->itemcount = $maxnumber; 00979 // Save the new value for options 00980 $DB->update_record('question_dataset_definitions', $newdef); 00981 } 00982 } 00983 } 00984 00985 if (isset($fromform->deletebutton)) { 00986 if (isset($fromform->selectdelete)) { 00987 $newmaxnumber = $maxnumber-$fromform->selectdelete; 00988 } else { 00989 $newmaxnumber = $maxnumber-1; 00990 } 00991 if ($newmaxnumber < 0) { 00992 $newmaxnumber = 0; 00993 } 00994 foreach ($datasetdefs as $datasetdef) { 00995 if ($datasetdef->itemcount == $maxnumber) { 00996 $datasetdef->itemcount= $newmaxnumber; 00997 $DB->update_record('question_dataset_definitions', $datasetdef); 00998 } 00999 } 01000 } 01001 } 01002 public function generate_dataset_item($options) { 01003 if (!preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~', 01004 $options, $regs)) { 01005 // Unknown options... 01006 return false; 01007 } 01008 if ($regs[1] == 'uniform') { 01009 $nbr = $regs[2] + ($regs[3]-$regs[2])*mt_rand()/mt_getrandmax(); 01010 return sprintf("%.".$regs[4].'f', $nbr); 01011 01012 } else if ($regs[1] == 'loguniform') { 01013 $log0 = log(abs($regs[2])); // It would have worked the other way to 01014 $nbr = exp($log0 + (log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax()); 01015 return sprintf("%.".$regs[4].'f', $nbr); 01016 01017 } else { 01018 print_error('disterror', 'question', '', $regs[1]); 01019 } 01020 return ''; 01021 } 01022 01023 public function comment_header($question) { 01024 $strheader = ''; 01025 $delimiter = ''; 01026 01027 $answers = $question->options->answers; 01028 01029 foreach ($answers as $key => $answer) { 01030 if (is_string($answer)) { 01031 $strheader .= $delimiter.$answer; 01032 } else { 01033 $strheader .= $delimiter.$answer->answer; 01034 } 01035 $delimiter = '<br/><br/><br/>'; 01036 } 01037 return $strheader; 01038 } 01039 01040 public function comment_on_datasetitems($qtypeobj, $questionid, $questiontext, 01041 $answers, $data, $number) { 01042 global $DB; 01043 $comment = new stdClass(); 01044 $comment->stranswers = array(); 01045 $comment->outsidelimit = false; 01046 $comment->answers = array(); 01047 // Find a default unit: 01048 if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units', 01049 array('question' => $questionid, 'multiplier' => 1.0))) { 01050 $unit = $unit->unit; 01051 } else { 01052 $unit = ''; 01053 } 01054 01055 $answers = fullclone($answers); 01056 $errors = ''; 01057 $delimiter = ': '; 01058 $virtualqtype = $qtypeobj->get_virtual_qtype(); 01059 foreach ($answers as $key => $answer) { 01060 $formula = $this->substitute_variables($answer->answer, $data); 01061 $formattedanswer = qtype_calculated_calculate_answer( 01062 $answer->answer, $data, $answer->tolerance, 01063 $answer->tolerancetype, $answer->correctanswerlength, 01064 $answer->correctanswerformat, $unit); 01065 if ($formula === '*') { 01066 $answer->min = ' '; 01067 $formattedanswer->answer = $answer->answer; 01068 } else { 01069 eval('$ansvalue = '.$formula.';'); 01070 $ans = new qtype_numerical_answer(0, $ansvalue, 0, '', 0, $answer->tolerance); 01071 $ans->tolerancetype = $answer->tolerancetype; 01072 list($answer->min, $answer->max) = $ans->get_tolerance_interval($answer); 01073 } 01074 if ($answer->min === '') { 01075 // This should mean that something is wrong 01076 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>'; 01077 } else if ($formula === '*') { 01078 $comment->stranswers[$key] = $formula . ' = ' . 01079 get_string('anyvalue', 'qtype_calculated') . '<br/><br/><br/>'; 01080 } else { 01081 $comment->stranswers[$key] = $formula . ' = ' . $formattedanswer->answer . '<br/>'; 01082 $correcttrue->correct = $formattedanswer->answer; 01083 $correcttrue->true = $answer->answer; 01084 if ($formattedanswer->answer < $answer->min || 01085 $formattedanswer->answer > $answer->max) { 01086 $comment->outsidelimit = true; 01087 $comment->answers[$key] = $key; 01088 $comment->stranswers[$key] .= 01089 get_string('trueansweroutsidelimits', 'qtype_calculated', $correcttrue); 01090 } else { 01091 $comment->stranswers[$key] .= 01092 get_string('trueanswerinsidelimits', 'qtype_calculated', $correcttrue); 01093 } 01094 $comment->stranswers[$key] .= '<br/>'; 01095 $comment->stranswers[$key] .= get_string('min', 'qtype_calculated') . 01096 $delimiter . $answer->min . ' --- '; 01097 $comment->stranswers[$key] .= get_string('max', 'qtype_calculated') . 01098 $delimiter . $answer->max; 01099 } 01100 } 01101 return fullclone($comment); 01102 } 01103 public function multichoice_comment_on_datasetitems($questionid, $questiontext, 01104 $answers, $data, $number) { 01105 global $DB; 01106 $comment = new stdClass(); 01107 $comment->stranswers = array(); 01108 $comment->outsidelimit = false; 01109 $comment->answers = array(); 01110 // Find a default unit: 01111 if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units', 01112 array('question' => $questionid, 'multiplier' => 1.0))) { 01113 $unit = $unit->unit; 01114 } else { 01115 $unit = ''; 01116 } 01117 01118 $answers = fullclone($answers); 01119 $errors = ''; 01120 $delimiter = ': '; 01121 foreach ($answers as $key => $answer) { 01122 $answer->answer = $this->substitute_variables($answer->answer, $data); 01123 //evaluate the equations i.e {=5+4) 01124 $qtext = ''; 01125 $qtextremaining = $answer->answer; 01126 while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) { 01127 $qtextsplits = explode($regs1[0], $qtextremaining, 2); 01128 $qtext = $qtext.$qtextsplits[0]; 01129 $qtextremaining = $qtextsplits[1]; 01130 if (empty($regs1[1])) { 01131 $str = ''; 01132 } else { 01133 if ($formulaerrors = qtype_calculated_find_formula_errors($regs1[1])) { 01134 $str = $formulaerrors; 01135 } else { 01136 eval('$str = '.$regs1[1].';'); 01137 01138 $texteval= qtype_calculated_calculate_answer( 01139 $str, $data, $answer->tolerance, 01140 $answer->tolerancetype, $answer->correctanswerlength, 01141 $answer->correctanswerformat, ''); 01142 $str = $texteval->answer; 01143 01144 } 01145 } 01146 $qtext = $qtext.$str; 01147 } 01148 $answer->answer = $qtext.$qtextremaining;; 01149 $comment->stranswers[$key]= $answer->answer; 01150 01151 } 01152 return fullclone($comment); 01153 } 01154 01155 public function tolerance_types() { 01156 return array( 01157 '1' => get_string('relative', 'qtype_numerical'), 01158 '2' => get_string('nominal', 'qtype_numerical'), 01159 '3' => get_string('geometric', 'qtype_numerical') 01160 ); 01161 } 01162 01163 public function dataset_options($form, $name, $mandatory = true, 01164 $renameabledatasets = false) { 01165 // Takes datasets from the parent implementation but 01166 // filters options that are currently not accepted by calculated 01167 // It also determines a default selection... 01168 // $renameabledatasets not implemented anmywhere 01169 list($options, $selected) = $this->dataset_options_from_database( 01170 $form, $name, '', 'qtype_calculated'); 01171 01172 foreach ($options as $key => $whatever) { 01173 if (!preg_match('~^1-~', $key) && $key != '0') { 01174 unset($options[$key]); 01175 } 01176 } 01177 if (!$selected) { 01178 if ($mandatory) { 01179 $selected = "1-0-$name"; // Default 01180 } else { 01181 $selected = '0'; // Default 01182 } 01183 } 01184 return array($options, $selected); 01185 } 01186 01187 public function construct_dataset_menus($form, $mandatorydatasets, 01188 $optionaldatasets) { 01189 global $OUTPUT; 01190 $datasetmenus = array(); 01191 foreach ($mandatorydatasets as $datasetname) { 01192 if (!isset($datasetmenus[$datasetname])) { 01193 list($options, $selected) = 01194 $this->dataset_options($form, $datasetname); 01195 unset($options['0']); // Mandatory... 01196 $datasetmenus[$datasetname] = html_writer::select( 01197 $options, 'dataset[]', $selected, null); 01198 } 01199 } 01200 foreach ($optionaldatasets as $datasetname) { 01201 if (!isset($datasetmenus[$datasetname])) { 01202 list($options, $selected) = 01203 $this->dataset_options($form, $datasetname); 01204 $datasetmenus[$datasetname] = html_writer::select( 01205 $options, 'dataset[]', $selected, null); 01206 } 01207 } 01208 return $datasetmenus; 01209 } 01210 01211 public function substitute_variables($str, $dataset) { 01212 global $OUTPUT; 01213 // testing for wrong numerical values 01214 // all calculations used this function so testing here should be OK 01215 01216 foreach ($dataset as $name => $value) { 01217 $val = $value; 01218 if (! is_numeric($val)) { 01219 $a = new stdClass(); 01220 $a->name = '{'.$name.'}'; 01221 $a->value = $value; 01222 echo $OUTPUT->notification(get_string('notvalidnumber', 'qtype_calculated', $a)); 01223 $val = 1.0; 01224 } 01225 if ($val < 0) { 01226 $str = str_replace('{'.$name.'}', '('.$val.')', $str); 01227 } else { 01228 $str = str_replace('{'.$name.'}', $val, $str); 01229 } 01230 } 01231 return $str; 01232 } 01233 01234 public function evaluate_equations($str, $dataset) { 01235 $formula = $this->substitute_variables($str, $dataset); 01236 if ($error = qtype_calculated_find_formula_errors($formula)) { 01237 return $error; 01238 } 01239 return $str; 01240 } 01241 01242 public function substitute_variables_and_eval($str, $dataset) { 01243 $formula = $this->substitute_variables($str, $dataset); 01244 if ($error = qtype_calculated_find_formula_errors($formula)) { 01245 return $error; 01246 } 01247 // Calculate the correct answer 01248 if (empty($formula)) { 01249 $str = ''; 01250 } else if ($formula === '*') { 01251 $str = '*'; 01252 } else { 01253 eval('$str = '.$formula.';'); 01254 } 01255 return $str; 01256 } 01257 01258 public function get_dataset_definitions($questionid, $newdatasets) { 01259 global $DB; 01260 //get the existing datasets for this question 01261 $datasetdefs = array(); 01262 if (!empty($questionid)) { 01263 global $CFG; 01264 $sql = "SELECT i.* 01265 FROM {question_datasets} d, {question_dataset_definitions} i 01266 WHERE d.question = ? AND d.datasetdefinition = i.id 01267 ORDER BY i.id"; 01268 if ($records = $DB->get_records_sql($sql, array($questionid))) { 01269 foreach ($records as $r) { 01270 $datasetdefs["$r->type-$r->category-$r->name"] = $r; 01271 } 01272 } 01273 } 01274 01275 foreach ($newdatasets as $dataset) { 01276 if (!$dataset) { 01277 continue; // The no dataset case... 01278 } 01279 01280 if (!isset($datasetdefs[$dataset])) { 01281 //make new datasetdef 01282 list($type, $category, $name) = explode('-', $dataset, 3); 01283 $datasetdef = new stdClass(); 01284 $datasetdef->type = $type; 01285 $datasetdef->name = $name; 01286 $datasetdef->category = $category; 01287 $datasetdef->itemcount = 0; 01288 $datasetdef->options = 'uniform:1.0:10.0:1'; 01289 $datasetdefs[$dataset] = clone($datasetdef); 01290 } 01291 } 01292 return $datasetdefs; 01293 } 01294 01295 public function save_dataset_definitions($form) { 01296 global $DB; 01297 // save synchronize 01298 01299 if (empty($form->dataset)) { 01300 $form->dataset = array(); 01301 } 01302 // Save datasets 01303 $datasetdefinitions = $this->get_dataset_definitions($form->id, $form->dataset); 01304 $tmpdatasets = array_flip($form->dataset); 01305 $defids = array_keys($datasetdefinitions); 01306 foreach ($defids as $defid) { 01307 $datasetdef = &$datasetdefinitions[$defid]; 01308 if (isset($datasetdef->id)) { 01309 if (!isset($tmpdatasets[$defid])) { 01310 // This dataset is not used any more, delete it 01311 $DB->delete_records('question_datasets', 01312 array('question' => $form->id, 'datasetdefinition' => $datasetdef->id)); 01313 if ($datasetdef->category == 0) { 01314 // Question local dataset 01315 $DB->delete_records('question_dataset_definitions', 01316 array('id' => $datasetdef->id)); 01317 $DB->delete_records('question_dataset_items', 01318 array('definition' => $datasetdef->id)); 01319 } 01320 } 01321 // This has already been saved or just got deleted 01322 unset($datasetdefinitions[$defid]); 01323 continue; 01324 } 01325 01326 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef); 01327 01328 if (0 != $datasetdef->category) { 01329 // We need to look for already existing 01330 // datasets in the category. 01331 // By first creating the datasetdefinition above we 01332 // can manage to automatically take care of 01333 // some possible realtime concurrence 01334 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions', 01335 'type = ? AND name = ? AND category = ? AND id < ? 01336 ORDER BY id DESC', 01337 array($datasetdef->type, $datasetdef->name, 01338 $datasetdef->category, $datasetdef->id))) { 01339 01340 while ($olderdatasetdef = array_shift($olderdatasetdefs)) { 01341 $DB->delete_records('question_dataset_definitions', 01342 array('id' => $datasetdef->id)); 01343 $datasetdef = $olderdatasetdef; 01344 } 01345 } 01346 } 01347 01348 // Create relation to this dataset: 01349 $questiondataset = new stdClass(); 01350 $questiondataset->question = $form->id; 01351 $questiondataset->datasetdefinition = $datasetdef->id; 01352 $DB->insert_record('question_datasets', $questiondataset); 01353 unset($datasetdefinitions[$defid]); 01354 } 01355 01356 // Remove local obsolete datasets as well as relations 01357 // to datasets in other categories: 01358 if (!empty($datasetdefinitions)) { 01359 foreach ($datasetdefinitions as $def) { 01360 $DB->delete_records('question_datasets', 01361 array('question' => $form->id, 'datasetdefinition' => $def->id)); 01362 01363 if ($def->category == 0) { // Question local dataset 01364 $DB->delete_records('question_dataset_definitions', 01365 array('id' => $def->id)); 01366 $DB->delete_records('question_dataset_items', 01367 array('definition' => $def->id)); 01368 } 01369 } 01370 } 01371 } 01377 public function save_as_new_dataset_definitions($form, $initialid) { 01378 global $CFG, $DB; 01379 // Get the datasets from the intial question 01380 $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset); 01381 // $tmpdatasets contains those of the new question 01382 $tmpdatasets = array_flip($form->dataset); 01383 $defids = array_keys($datasetdefinitions);// new datasets 01384 foreach ($defids as $defid) { 01385 $datasetdef = &$datasetdefinitions[$defid]; 01386 if (isset($datasetdef->id)) { 01387 // This dataset exist in the initial question 01388 if (!isset($tmpdatasets[$defid])) { 01389 // do not exist in the new question so ignore 01390 unset($datasetdefinitions[$defid]); 01391 continue; 01392 } 01393 // create a copy but not for category one 01394 if (0 == $datasetdef->category) { 01395 $olddatasetid = $datasetdef->id; 01396 $olditemcount = $datasetdef->itemcount; 01397 $datasetdef->itemcount = 0; 01398 $datasetdef->id = $DB->insert_record('question_dataset_definitions', 01399 $datasetdef); 01400 //copy the dataitems 01401 $olditems = $this->get_database_dataset_items($olddatasetid); 01402 if (count($olditems) > 0) { 01403 $itemcount = 0; 01404 foreach ($olditems as $item) { 01405 $item->definition = $datasetdef->id; 01406 $DB->insert_record('question_dataset_items', $item); 01407 $itemcount++; 01408 } 01409 //update item count to olditemcount if 01410 // at least this number of items has been recover from the database 01411 if ($olditemcount <= $itemcount) { 01412 $datasetdef->itemcount = $olditemcount; 01413 } else { 01414 $datasetdef->itemcount = $itemcount; 01415 } 01416 $DB->update_record('question_dataset_definitions', $datasetdef); 01417 } // end of copy the dataitems 01418 }// end of copy the datasetdef 01419 // Create relation to the new question with this 01420 // copy as new datasetdef from the initial question 01421 $questiondataset = new stdClass(); 01422 $questiondataset->question = $form->id; 01423 $questiondataset->datasetdefinition = $datasetdef->id; 01424 $DB->insert_record('question_datasets', $questiondataset); 01425 unset($datasetdefinitions[$defid]); 01426 continue; 01427 }// end of datasetdefs from the initial question 01428 // really new one code similar to save_dataset_definitions() 01429 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef); 01430 01431 if (0 != $datasetdef->category) { 01432 // We need to look for already existing 01433 // datasets in the category. 01434 // By first creating the datasetdefinition above we 01435 // can manage to automatically take care of 01436 // some possible realtime concurrence 01437 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions', 01438 "type = ? AND name = ? AND category = ? AND id < ? 01439 ORDER BY id DESC", 01440 array($datasetdef->type, $datasetdef->name, 01441 $datasetdef->category, $datasetdef->id))) { 01442 01443 while ($olderdatasetdef = array_shift($olderdatasetdefs)) { 01444 $DB->delete_records('question_dataset_definitions', 01445 array('id' => $datasetdef->id)); 01446 $datasetdef = $olderdatasetdef; 01447 } 01448 } 01449 } 01450 01451 // Create relation to this dataset: 01452 $questiondataset = new stdClass(); 01453 $questiondataset->question = $form->id; 01454 $questiondataset->datasetdefinition = $datasetdef->id; 01455 $DB->insert_record('question_datasets', $questiondataset); 01456 unset($datasetdefinitions[$defid]); 01457 } 01458 01459 // Remove local obsolete datasets as well as relations 01460 // to datasets in other categories: 01461 if (!empty($datasetdefinitions)) { 01462 foreach ($datasetdefinitions as $def) { 01463 $DB->delete_records('question_datasets', 01464 array('question' => $form->id, 'datasetdefinition' => $def->id)); 01465 01466 if ($def->category == 0) { // Question local dataset 01467 $DB->delete_records('question_dataset_definitions', 01468 array('id' => $def->id)); 01469 $DB->delete_records('question_dataset_items', 01470 array('definition' => $def->id)); 01471 } 01472 } 01473 } 01474 } 01475 01476 // Dataset functionality 01477 public function pick_question_dataset($question, $datasetitem) { 01478 // Select a dataset in the following format: 01479 // An array indexed by the variable names (d.name) pointing to the value 01480 // to be substituted 01481 global $CFG, $DB; 01482 if (!$dataitems = $DB->get_records_sql( 01483 "SELECT i.id, d.name, i.value 01484 FROM {question_dataset_definitions} d, 01485 {question_dataset_items} i, 01486 {question_datasets} q 01487 WHERE q.question = ? 01488 AND q.datasetdefinition = d.id 01489 AND d.id = i.definition 01490 AND i.itemnumber = ? 01491 ORDER BY i.id DESC ", array($question->id, $datasetitem))) { 01492 $a = new stdClass(); 01493 $a->id = $question->id; 01494 $a->item = $datasetitem; 01495 print_error('cannotgetdsfordependent', 'question', '', $a); 01496 } 01497 $dataset = Array(); 01498 foreach ($dataitems as $id => $dataitem) { 01499 if (!isset($dataset[$dataitem->name])) { 01500 $dataset[$dataitem->name] = $dataitem->value; 01501 } 01502 } 01503 return $dataset; 01504 } 01505 01506 public function dataset_options_from_database($form, $name, $prefix = '', 01507 $langfile = 'qtype_calculated') { 01508 global $CFG, $DB; 01509 $type = 1; // only type = 1 (i.e. old 'LITERAL') has ever been used 01510 01511 // First options - it is not a dataset... 01512 $options['0'] = get_string($prefix.'nodataset', $langfile); 01513 // new question no local 01514 if (!isset($form->id) || $form->id == 0) { 01515 $key = "$type-0-$name"; 01516 $options[$key] = get_string($prefix."newlocal$type", $langfile); 01517 $currentdatasetdef = new stdClass(); 01518 $currentdatasetdef->type = '0'; 01519 } else { 01520 01521 // Construct question local options 01522 $sql = "SELECT a.* 01523 FROM {question_dataset_definitions} a, {question_datasets} b 01524 WHERE a.id = b.datasetdefinition AND a.type = '1' AND b.question = ? AND a.name = ?"; 01525 $currentdatasetdef = $DB->get_record_sql($sql, array($form->id, $name)); 01526 if (!$currentdatasetdef) { 01527 $currentdatasetdef->type = '0'; 01528 } 01529 $key = "$type-0-$name"; 01530 if ($currentdatasetdef->type == $type 01531 and $currentdatasetdef->category == 0) { 01532 $options[$key] = get_string($prefix."keptlocal$type", $langfile); 01533 } else { 01534 $options[$key] = get_string($prefix."newlocal$type", $langfile); 01535 } 01536 } 01537 // Construct question category options 01538 $categorydatasetdefs = $DB->get_records_sql( 01539 "SELECT b.question, a.* 01540 FROM {question_datasets} b, 01541 {question_dataset_definitions} a 01542 WHERE a.id = b.datasetdefinition 01543 AND a.type = '1' 01544 AND a.category = ? 01545 AND a.name = ?", array($form->category, $name)); 01546 $type = 1; 01547 $key = "$type-$form->category-$name"; 01548 if (!empty($categorydatasetdefs)) { 01549 // there is at least one with the same name 01550 if (isset($form->id) && isset($categorydatasetdefs[$form->id])) { 01551 // it is already used by this question 01552 $options[$key] = get_string($prefix."keptcategory$type", $langfile); 01553 } else { 01554 $options[$key] = get_string($prefix."existingcategory$type", $langfile); 01555 } 01556 } else { 01557 $options[$key] = get_string($prefix."newcategory$type", $langfile); 01558 } 01559 // All done! 01560 return array($options, $currentdatasetdef->type 01561 ? "$currentdatasetdef->type-$currentdatasetdef->category-$name" 01562 : ''); 01563 } 01564 01565 public function find_dataset_names($text) { 01566 // Returns the possible dataset names found in the text as an array 01567 // The array has the dataset name for both key and value 01568 $datasetnames = array(); 01569 while (preg_match('~\\{([[:alpha:]][^>} <{"\']*)\\}~', $text, $regs)) { 01570 $datasetnames[$regs[1]] = $regs[1]; 01571 $text = str_replace($regs[0], '', $text); 01572 } 01573 return $datasetnames; 01574 } 01575 01581 public function get_dataset_definitions_category($form) { 01582 global $CFG, $DB; 01583 $datasetdefs = array(); 01584 $lnamemax = 30; 01585 if (!empty($form->category)) { 01586 $sql = "SELECT i.*, d.* 01587 FROM {question_datasets} d, {question_dataset_definitions} i 01588 WHERE i.id = d.datasetdefinition AND i.category = ?"; 01589 if ($records = $DB->get_records_sql($sql, array($form->category))) { 01590 foreach ($records as $r) { 01591 if (!isset ($datasetdefs["$r->name"])) { 01592 $datasetdefs["$r->name"] = $r->itemcount; 01593 } 01594 } 01595 } 01596 } 01597 return $datasetdefs; 01598 } 01599 01607 public function print_dataset_definitions_category($form) { 01608 global $CFG, $DB; 01609 $datasetdefs = array(); 01610 $lnamemax = 22; 01611 $namestr = get_string('name'); 01612 $rangeofvaluestr = get_string('minmax', 'qtype_calculated'); 01613 $questionusingstr = get_string('usedinquestion', 'qtype_calculated'); 01614 $itemscountstr = get_string('itemscount', 'qtype_calculated'); 01615 $text = ''; 01616 if (!empty($form->category)) { 01617 list($category) = explode(',', $form->category); 01618 $sql = "SELECT i.*, d.* 01619 FROM {question_datasets} d, 01620 {question_dataset_definitions} i 01621 WHERE i.id = d.datasetdefinition 01622 AND i.category = ?"; 01623 if ($records = $DB->get_records_sql($sql, array($category))) { 01624 foreach ($records as $r) { 01625 $sql1 = "SELECT q.* 01626 FROM {question} q 01627 WHERE q.id = ?"; 01628 if (!isset ($datasetdefs["$r->type-$r->category-$r->name"])) { 01629 $datasetdefs["$r->type-$r->category-$r->name"]= $r; 01630 } 01631 if ($questionb = $DB->get_records_sql($sql1, array($r->question))) { 01632 $datasetdefs["$r->type-$r->category-$r->name"]->questions[ 01633 $r->question]->name = $questionb[$r->question]->name; 01634 } 01635 } 01636 } 01637 } 01638 if (!empty ($datasetdefs)) { 01639 01640 $text = "<table width=\"100%\" border=\"1\"><tr> 01641 <th style=\"white-space:nowrap;\" class=\"header\" 01642 scope=\"col\">$namestr</th> 01643 <th style=\"white-space:nowrap;\" class=\"header\" 01644 scope=\"col\">$rangeofvaluestr</th> 01645 <th style=\"white-space:nowrap;\" class=\"header\" 01646 scope=\"col\">$itemscountstr</th> 01647 <th style=\"white-space:nowrap;\" class=\"header\" 01648 scope=\"col\">$questionusingstr</th> 01649 </tr>"; 01650 foreach ($datasetdefs as $datasetdef) { 01651 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4); 01652 $text .= "<tr> 01653 <td valign=\"top\" align=\"center\">$datasetdef->name</td> 01654 <td align=\"center\" valign=\"top\">$min <strong>-</strong> $max</td> 01655 <td align=\"right\" valign=\"top\">$datasetdef->itemcount </td> 01656 <td align=\"left\">"; 01657 foreach ($datasetdef->questions as $qu) { 01658 //limit the name length displayed 01659 if (!empty($qu->name)) { 01660 $qu->name = (strlen($qu->name) > $lnamemax) ? 01661 substr($qu->name, 0, $lnamemax).'...' : $qu->name; 01662 } else { 01663 $qu->name = ''; 01664 } 01665 $text .= " $qu->name <br/>"; 01666 } 01667 $text .= "</td></tr>"; 01668 } 01669 $text .= "</table>"; 01670 } else { 01671 $text .= get_string('nosharedwildcard', 'qtype_calculated'); 01672 } 01673 return $text; 01674 } 01675 01684 public function print_dataset_definitions_category_shared($question, $datasetdefsq) { 01685 global $CFG, $DB; 01686 $datasetdefs = array(); 01687 $lnamemax = 22; 01688 $namestr = get_string('name', 'quiz'); 01689 $rangeofvaluestr = get_string('minmax', 'qtype_calculated'); 01690 $questionusingstr = get_string('usedinquestion', 'qtype_calculated'); 01691 $itemscountstr = get_string('itemscount', 'qtype_calculated'); 01692 $text = ''; 01693 if (!empty($question->category)) { 01694 list($category) = explode(',', $question->category); 01695 $sql = "SELECT i.*, d.* 01696 FROM {question_datasets} d, {question_dataset_definitions} i 01697 WHERE i.id = d.datasetdefinition AND i.category = ?"; 01698 if ($records = $DB->get_records_sql($sql, array($category))) { 01699 foreach ($records as $r) { 01700 $sql1 = "SELECT q.* 01701 FROM {question} q 01702 WHERE q.id = ?"; 01703 if (!isset ($datasetdefs["$r->type-$r->category-$r->name"])) { 01704 $datasetdefs["$r->type-$r->category-$r->name"]= $r; 01705 } 01706 if ($questionb = $DB->get_records_sql($sql1, array($r->question))) { 01707 $datasetdefs["$r->type-$r->category-$r->name"]->questions[ 01708 $r->question]->name = $questionb[$r->question]->name; 01709 $datasetdefs["$r->type-$r->category-$r->name"]->questions[ 01710 $r->question]->id = $questionb[$r->question]->id; 01711 } 01712 } 01713 } 01714 } 01715 if (!empty ($datasetdefs)) { 01716 01717 $text = "<table width=\"100%\" border=\"1\"><tr> 01718 <th style=\"white-space:nowrap;\" class=\"header\" 01719 scope=\"col\">$namestr</th>"; 01720 $text .= "<th style=\"white-space:nowrap;\" class=\"header\" 01721 scope=\"col\">$itemscountstr</th>"; 01722 $text .= "<th style=\"white-space:nowrap;\" class=\"header\" 01723 scope=\"col\"> $questionusingstr </th>"; 01724 $text .= "<th style=\"white-space:nowrap;\" class=\"header\" 01725 scope=\"col\">Quiz</th>"; 01726 $text .= "<th style=\"white-space:nowrap;\" class=\"header\" 01727 scope=\"col\">Attempts</th></tr>"; 01728 foreach ($datasetdefs as $datasetdef) { 01729 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4); 01730 $count = count($datasetdef->questions); 01731 $text .= "<tr> 01732 <td style=\"white-space:nowrap;\" valign=\"top\" 01733 align=\"center\" rowspan=\"$count\"> $datasetdef->name </td> 01734 <td align=\"right\" valign=\"top\" 01735 rowspan=\"$count\">$datasetdef->itemcount</td>"; 01736 $line = 0; 01737 foreach ($datasetdef->questions as $qu) { 01738 //limit the name length displayed 01739 if (!empty($qu->name)) { 01740 $qu->name = (strlen($qu->name) > $lnamemax) ? 01741 substr($qu->name, 0, $lnamemax).'...' : $qu->name; 01742 } else { 01743 $qu->name = ''; 01744 } 01745 if ($line) { 01746 $text .= "<tr>"; 01747 } 01748 $line++; 01749 $text .= "<td align=\"left\" style=\"white-space:nowrap;\">$qu->name</td>"; 01750 $nbofquiz = 0; 01751 $nbofattempts= 0; 01752 $usedinquiz = false; 01753 if ($list = $DB->get_records('quiz_question_instances', 01754 array('question' => $qu->id))) { 01755 $usedinquiz = true; 01756 foreach ($list as $key => $li) { 01757 $nbofquiz ++; 01758 if ($att = $DB->get_records('quiz_attempts', 01759 array('quiz' => $li->quiz, 'preview' => '0'))) { 01760 $nbofattempts+= count($att); 01761 } 01762 } 01763 } 01764 if ($usedinquiz) { 01765 $text .= "<td align=\"center\">$nbofquiz</td>"; 01766 } else { 01767 $text .= "<td align=\"center\">0</td>"; 01768 } 01769 if ($usedinquiz) { 01770 $text .= "<td align=\"center\">$nbofattempts"; 01771 } else { 01772 $text .= "<td align=\"left\"><br/>"; 01773 } 01774 01775 $text .= "</td></tr>"; 01776 } 01777 } 01778 $text .= "</table>"; 01779 } else { 01780 $text .= get_string('nosharedwildcard', 'qtype_calculated'); 01781 } 01782 return $text; 01783 } 01784 01785 public function find_math_equations($text) { 01786 // Returns the possible dataset names found in the text as an array 01787 // The array has the dataset name for both key and value 01788 $equations = array(); 01789 while (preg_match('~\{=([^[:space:]}]*)}~', $text, $regs)) { 01790 $equations[] = $regs[1]; 01791 $text = str_replace($regs[0], '', $text); 01792 } 01793 return $equations; 01794 } 01795 01796 public function get_virtual_qtype() { 01797 return question_bank::get_qtype('numerical'); 01798 } 01799 01800 public function get_possible_responses($questiondata) { 01801 $responses = array(); 01802 01803 $virtualqtype = $this->get_virtual_qtype(); 01804 $unit = $virtualqtype->get_default_numerical_unit($questiondata); 01805 01806 $tolerancetypes = $this->tolerance_types(); 01807 01808 $starfound = false; 01809 foreach ($questiondata->options->answers as $aid => $answer) { 01810 $responseclass = $answer->answer; 01811 01812 if ($responseclass === '*') { 01813 $starfound = true; 01814 } else { 01815 $a = new stdClass(); 01816 $a->answer = $virtualqtype->add_unit($questiondata, $responseclass, $unit); 01817 $a->tolerance = $answer->tolerance; 01818 $a->tolerancetype = $tolerancetypes[$answer->tolerancetype]; 01819 01820 $responseclass = get_string('answerwithtolerance', 'qtype_calculated', $a); 01821 } 01822 01823 $responses[$aid] = new question_possible_response($responseclass, 01824 $answer->fraction); 01825 } 01826 01827 if (!$starfound) { 01828 $responses[0] = new question_possible_response( 01829 get_string('didnotmatchanyanswer', 'question'), 0); 01830 } 01831 01832 $responses[null] = question_possible_response::no_response(); 01833 01834 return array($questiondata->id => $responses); 01835 } 01836 01837 public function move_files($questionid, $oldcontextid, $newcontextid) { 01838 $fs = get_file_storage(); 01839 01840 parent::move_files($questionid, $oldcontextid, $newcontextid); 01841 $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid); 01842 } 01843 01844 protected function delete_files($questionid, $contextid) { 01845 $fs = get_file_storage(); 01846 01847 parent::delete_files($questionid, $contextid); 01848 $this->delete_files_in_answers($questionid, $contextid); 01849 } 01850 } 01851 01852 01853 function qtype_calculated_calculate_answer($formula, $individualdata, 01854 $tolerance, $tolerancetype, $answerlength, $answerformat = '1', $unit = '') { 01855 // The return value has these properties: 01856 // ->answer the correct answer 01857 // ->min the lower bound for an acceptable response 01858 // ->max the upper bound for an accetpable response 01859 01860 // Exchange formula variables with the correct values... 01861 $answer = question_bank::get_qtype('calculated')->substitute_variables_and_eval( 01862 $formula, $individualdata); 01863 if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */ 01864 /*** Adjust to the correct number of decimals ***/ 01865 if (stripos($answer, 'e')>0) { 01866 $answerlengthadd = strlen($answer)-stripos($answer, 'e'); 01867 } else { 01868 $answerlengthadd = 0; 01869 } 01870 $calculated->answer = round(floatval($answer), $answerlength+$answerlengthadd); 01871 01872 if ($answerlength) { 01873 /* Try to include missing zeros at the end */ 01874 01875 if (preg_match('~^(.*\\.)(.*)$~', $calculated->answer, $regs)) { 01876 $calculated->answer = $regs[1] . substr( 01877 $regs[2] . '00000000000000000000000000000000000000000x', 01878 0, $answerlength) 01879 . $unit; 01880 } else { 01881 $calculated->answer .= 01882 substr('.00000000000000000000000000000000000000000x', 01883 0, $answerlength + 1) . $unit; 01884 } 01885 } else { 01886 /* Attach unit */ 01887 $calculated->answer .= $unit; 01888 } 01889 01890 } else if ($answer) { // Significant figures does only apply if the result is non-zero 01891 01892 // Convert to positive answer... 01893 if ($answer < 0) { 01894 $answer = -$answer; 01895 $sign = '-'; 01896 } else { 01897 $sign = ''; 01898 } 01899 01900 // Determine the format 0.[1-9][0-9]* for the answer... 01901 $p10 = 0; 01902 while ($answer < 1) { 01903 --$p10; 01904 $answer *= 10; 01905 } 01906 while ($answer >= 1) { 01907 ++$p10; 01908 $answer /= 10; 01909 } 01910 // ... and have the answer rounded of to the correct length 01911 $answer = round($answer, $answerlength); 01912 01913 //if we rounded up to 1.0, place the answer back into 0.[1-9][0-9]* format 01914 if ($answer >= 1) { 01915 ++$p10; 01916 $answer /= 10; 01917 } 01918 01919 // Have the answer written on a suitable format, 01920 // Either scientific or plain numeric 01921 if (-2 > $p10 || 4 < $p10) { 01922 // Use scientific format: 01923 $exponent = 'e'.--$p10; 01924 $answer *= 10; 01925 if (1 == $answerlength) { 01926 $calculated->answer = $sign.$answer.$exponent.$unit; 01927 } else { 01928 // Attach additional zeros at the end of $answer, 01929 $answer .= (1 == strlen($answer) ? '.' : '') 01930 . '00000000000000000000000000000000000000000x'; 01931 $calculated->answer = $sign 01932 .substr($answer, 0, $answerlength +1).$exponent.$unit; 01933 } 01934 } else { 01935 // Stick to plain numeric format 01936 $answer *= "1e$p10"; 01937 if (0.1 <= $answer / "1e$answerlength") { 01938 $calculated->answer = $sign.$answer.$unit; 01939 } else { 01940 // Could be an idea to add some zeros here 01941 $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '') 01942 . '00000000000000000000000000000000000000000x'; 01943 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1); 01944 $calculated->answer = $sign.substr($answer, 0, $oklen).$unit; 01945 } 01946 } 01947 01948 } else { 01949 $calculated->answer = 0.0; 01950 } 01951 01952 // Return the result 01953 return $calculated; 01954 } 01955 01956 01957 function qtype_calculated_find_formula_errors($formula) { 01958 // Validates the formula submitted from the question edit page. 01959 // Returns false if everything is alright. 01960 // Otherwise it constructs an error message 01961 // Strip away dataset names 01962 while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) { 01963 $formula = str_replace($regs[0], '1', $formula); 01964 } 01965 01966 // Strip away empty space and lowercase it 01967 $formula = strtolower(str_replace(' ', '', $formula)); 01968 01969 $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */ 01970 $operatorornumber = "[$safeoperatorchar.0-9eE]"; 01971 01972 while (preg_match("~(^|[$safeoperatorchar,(])([a-z0-9_]*)" . 01973 "\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)~", 01974 $formula, $regs)) { 01975 switch ($regs[2]) { 01976 // Simple parenthesis 01977 case '': 01978 if ((isset($regs[4]) && $regs[4]) || strlen($regs[3]) == 0) { 01979 return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]); 01980 } 01981 break; 01982 01983 // Zero argument functions 01984 case 'pi': 01985 if ($regs[3]) { 01986 return get_string('functiontakesnoargs', 'qtype_calculated', $regs[2]); 01987 } 01988 break; 01989 01990 // Single argument functions (the most common case) 01991 case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh': 01992 case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos': 01993 case 'cosh': case 'decbin': case 'decoct': case 'deg2rad': 01994 case 'exp': case 'expm1': case 'floor': case 'is_finite': 01995 case 'is_infinite': case 'is_nan': case 'log10': case 'log1p': 01996 case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt': 01997 case 'tan': case 'tanh': 01998 if (!empty($regs[4]) || empty($regs[3])) { 01999 return get_string('functiontakesonearg', 'qtype_calculated', $regs[2]); 02000 } 02001 break; 02002 02003 // Functions that take one or two arguments 02004 case 'log': case 'round': 02005 if (!empty($regs[5]) || empty($regs[3])) { 02006 return get_string('functiontakesoneortwoargs', 'qtype_calculated', $regs[2]); 02007 } 02008 break; 02009 02010 // Functions that must have two arguments 02011 case 'atan2': case 'fmod': case 'pow': 02012 if (!empty($regs[5]) || empty($regs[4])) { 02013 return get_string('functiontakestwoargs', 'qtype_calculated', $regs[2]); 02014 } 02015 break; 02016 02017 // Functions that take two or more arguments 02018 case 'min': case 'max': 02019 if (empty($regs[4])) { 02020 return get_string('functiontakesatleasttwo', 'qtype_calculated', $regs[2]); 02021 } 02022 break; 02023 02024 default: 02025 return get_string('unsupportedformulafunction', 'qtype_calculated', $regs[2]); 02026 } 02027 02028 // Exchange the function call with '1' and then chack for 02029 // another function call... 02030 if ($regs[1]) { 02031 // The function call is proceeded by an operator 02032 $formula = str_replace($regs[0], $regs[1] . '1', $formula); 02033 } else { 02034 // The function call starts the formula 02035 $formula = preg_replace("~^$regs[2]\\([^)]*\\)~", '1', $formula); 02036 } 02037 } 02038 02039 if (preg_match("~[^$safeoperatorchar.0-9eE]+~", $formula, $regs)) { 02040 return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]); 02041 } else { 02042 // Formula just might be valid 02043 return false; 02044 } 02045 }